Python

De computertaal Python werd ooorspronkelijk ontworpen als een open source computertaal die voor iedereen makkelijk te leren en te programmeren zou zijn. Dat verklaart de enorme populariteit ervan. Maar door die enorme populariteit is het in de loop der tijd toch ook een heel complexe taal geworden. Er zijn eindeloos veel boeken verschenen over Python. De inhoud van deze boeken wil ik niet herhalen. Deze pagina gaat ervanuit dat je met de eerste beginselen van de taal al kennisgemaakt hebt. Op deze webpagina verzamel ik allerlei weetjes die ik makkelijk wil kunnen terugvinden.

User input

Met de functie input() kun je iemand iets laten intypen. Wat de gebruiker intikt wordt altijd als 'string' geïnterpreteerd. Dat betekent dat Python de ingetypte tekst ziet als een opeenvolging van lettertekens zonder enige betekenis. Als je dus 12 intikt, dan ziet Python dat niet per se als een getal, maar als een één gevolgd door een twee. Mocht je willen dat wat ingetikt wordt anders wordt opgevat, moet je het ingetikte converteren naar het gewenste gegevenstype.

def main():
    x = input('x = ')
    print(type(x))
    if x.isnumeric():
        x = int(x)
        print(type(x))

if __name__ == '__main__':
    main()

Gegevenstypen

Python kent verschillende soorten gegevens. De meest bekende zijn:

letterscharactersstr()
gehele getallenintegersint()
drijvende-komma-getallenfloating point numbersfloat()
complexe getallencomplex numbers
waar of niet waarBooleans

In Python wordt geen decimale komma gebruikt, maar een decimale punt.

Een bestand aanmaken

Een bestand kun je met de volgende code aanmaken:

def main():
    file_object = open('outfile.csv', 'w')
    file_object.write("Hello\n")
    file_object.close()

if __name__ == '__main__':
    main()        

Een bestand inlezen

Voor mijn privé-programma's, vind ik csv-bestanden inlezen de gemakkelijkste vorm van invoer. Csv-bestanden kun je makkelijk aanmaken met Kladblok of Excel. Mijn voorkeur gaat uit naar de puntkomma als scheidingsteken. Het inlezen van een bestand kan in Python op verschillende manieren. De eerste manier is door een bestand te openen voor lezen, vervolgens het bestand te doorlopen met een for-loop. Bij een csv-bestand kun je in die for-loop elk record splitsen in verschillende velden. Na verwerking moet je het bestand weer te sluiten.

def main():
    file_object = open('myfile.csv', 'r')
    for line in file_object:
        print(line.strip('\n'))
        velden = line.split(';')
        for v in velden:
            print(v)
    file_object.close()
    
if __name__ == '__main__':
    main() 

Een andere manier om een bestand in te lezen gaat met behulp van een with-context. Daarbij hoef je de file niet te sluiten, want de context van het with-statement zorgt ervoor dat dat gebeurt :

import sys

def main():
    filename = 'MyFile.txt'
    try:
        with open(filename) as f_input:
            for line in f_input:
                line = line.strip('\n')
                print(line)
    except Exception as err:
        print('(1) err')
        print( err )
        print('(2) sys.exc_info()[0]')
        print( sys.exc_info()[0] )          # exception class
        print('(3) sys.exc_info()[1]')
        print( sys.exc_info()[1] )          # value
        print('(4) sys.exc_info()[2]')
        print( sys.exc_info()[2] )          # traceback object
        print('=====')         
if __name__ == '__main__':
    main()  

Als het bestand MyFile.txt niet bestaat, geeft het programma de volgende output:

(1) err
[Errno 2] No such file or directory: 'MyFile.txt'
(2) sys.exc_info()[0]
<class 'FileNotFoundError'>
(3) sys.exc_info()[1]
[Errno 2] No such file or directory: 'MyFile.txt'
(4) sys.exc_info()[2]
<traceback object at 0x000002C6B50EB200>
=====

Exceptions

Je kunt het afhandelen vvan de fout in bovenstaand programma wat eleganter laten verlopen door de de class die in bovenstaande foutmelding werd genoemd, afzonderlijk af te handelen:

import sys

def main():
    filename = 'MyFile.txt'
    try:
        with open(filename) as f_input:
            for line in f_input:
                line = line.strip('\n')
                print(line)
    except FileNotFoundError:
        print('Bestand ' + filename + ' is niet aanwezig')
    except Exception as err:
        print('(1) err')
        print( err )
        print('(2) sys.exc_info()[0]')
        print( sys.exc_info()[0] )          # exception class
        print('(3) sys.exc_info()[1]')
        print( sys.exc_info()[1] )          # value
        print('(4) sys.exc_info()[2]')
        print( sys.exc_info()[2] )          # traceback object
        print('=====')         
if __name__ == '__main__':
    main()  

Als je de html-file die gaat over CSS, als invoerbestand neemt, wordt het programma maar deels uitgevoerd. Het eindigt met de volgende regels:

Als de viewport breed genoeg is, staan de verschillende blokken naast elkaar.
(1) err
'charmap' codec can't decode byte 0x9d in position 3699: character maps to 
(2) sys.exc_info()[0]

(3) sys.exc_info()[1]
'charmap' codec can't decode byte 0x9d in position 3699: character maps to 
(4) sys.exc_info()[2]
<traceback object at 0x000001FA93C32E80>
=====

Deze informatie is niet voldoende om te weten wat er aan de hand is. We breiden de code uit:

import sys, traceback

def main():
    filename = 'MyFile.txt'
    try:
        with open(filename) as f_input:
            for line in f_input:
                line = line.strip('\n')
                print(line)
    except FileNotFoundError:
        print('Bestand ' + filename + ' is niet aanwezig')
    except Exception as err:
        print('(1) err')
        print( err )
        print('(2) sys.exc_info()[0]')
        print( sys.exc_info()[0] )          # exception class
        print('(3) sys.exc_info()[1]')
        print( sys.exc_info()[1] )          # value
        print('(4) sys.exc_info()[2]')
        print( sys.exc_info()[2] )          # traceback object
        print('(5) err.object')
        print( err.object )
        print('(6) traceback.print_exception(err)')
        traceback.print_exception(err)
        print('(7) traceback.print_tb(err)')
        traceback.print_tb(err)             # traceback
        print('=====')         
if __name__ == '__main__':
    main()  

De output wordt:

            Als de viewport breed genoeg is, staan de verschillende blokken naast elkaar.
(1) err
'charmap' codec can't decode byte 0x9d in position 3699: character maps to <undefined>
(2) sys.exc_info()[0]
<class 'UnicodeDecodeError'>
(3) sys.exc_info()[1]
'charmap' codec can't decode byte 0x9d in position 3699: character maps to <undefined>
(4) sys.exc_info()[2]
<traceback object at 0x0000022A10433940>
(5) err.object
Squeezed text (64 lines).
(6) traceback.print_exception(err)
Traceback (most recent call last):
  File "E:\Site\Site20240516\prive\ict\extra\test0001.py", line 7, in main
    for line in f_input:
  File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\encodings\cp1252.py", line 23, in decode
    return codecs.charmap_decode(input,self.errors,decoding_table)[0]
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
UnicodeDecodeError: 'charmap' codec can't decode byte 0x9d in position 3699: character maps to <undefined>
(7) traceback.print_tb(err)
Traceback (most recent call last):
  File "E:\Site\Site20240516\prive\ict\extra\test0001.py", line 7, in main
    for line in f_input:
  File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\encodings\cp1252.py", line 23, in decode
    return codecs.charmap_decode(input,self.errors,decoding_table)[0]
UnicodeDecodeError: 'charmap' codec can't decode byte 0x9d in position 3699: character maps to <undefined>

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "E:\Site\Site20240516\prive\ict\extra\test0001.py", line 29, in <module>
    main()
  File "E:\Site\Site20240516\prive\ict\extra\test0001.py", line 26, in main
    traceback.print_tb(err)             # traceback
  File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\traceback.py", line 55, in print_tb
    print_list(extract_tb(tb, limit=limit), file=file)
  File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\traceback.py", line 74, in extract_tb
    return StackSummary._extract_from_extended_frame_gen(
  File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\traceback.py", line 418, in _extract_from_extended_frame_gen
    for f, (lineno, end_lineno, colno, end_colno) in frame_gen:
  File "C:\Users\Wim\AppData\Local\Programs\Python\Python312\Lib\traceback.py", line 355, in _walk_tb_with_full_positions
    positions = _get_code_position(tb.tb_frame.f_code, tb.tb_lasti)
AttributeError: 'UnicodeDecodeError' object has no attribute 'tb_frame'

Merk op dat het statement "print('=====')" niet wordt uitgevoerd. Het programma is na het afdrukken van de foutmeldingen afgebroken.

Deze foutmelding is te complex voor mij. Ik ga op zoek op Internet, en ontdek dat anderen al meer dan 6 jaar eerder met deze foutmelding worstelden. Als mogelijke oorzaak wordt geopperd dat het bestand niet met utf-8 is aangemaakt. Ik heb de input-file aangemaakt met behulp van het Kladblok-programma in Windows. Windows schijnt niet met utf-8 te werken. De suggesties die op stackoverklow.com worden gegeven, zijn:

De eerste suggestie leidt tot:

import sys, traceback

def main():
    filename = 'MyFile.txt'
    tel = 0
    try:
        with open(filename, 'rb') as f_input:
            for b_line in f_input:
                line = b_line.decode('utf-8')
                line = line.strip('\n')
                print(line)
    except FileNotFoundError:
        print('Bestand ' + filename + ' is niet aanwezig')
    except Exception as err:
        print('(1) err')
        print( err )
        print('(2) sys.exc_info()[0]')
        print( sys.exc_info()[0] )          # exception class
        print('(3) sys.exc_info()[1]')
        print( sys.exc_info()[1] )          # value
        print('(4) sys.exc_info()[2]')
        print( sys.exc_info()[2] )          # traceback object
        print('(5) err.object')
        print( err.object )
        print('(6) traceback.print_exception(err)')
        traceback.print_exception(err)
        print('(7) traceback.print_tb(err)')
        traceback.print_tb(err)             # traceback
        print('=====')         
if __name__ == '__main__':
    main()  

While-statement

Het volgende programma telt een aantal getallen bij elkaar op.

def main():
    numbers = [1, 2, 3, 5, 7, 11, 
               13, 17, 19, 23, 29]
    quantity = len(numbers)
    total = 0
    i = 0
    while i < quantity:
        total = total + numbers[i]
        i = i + 1
    print(quantity)
    print(total)

if __name__ == '__main__':
    main()








In het while-statement wordt de voorwaarde 'i < quantity' getest. Er zijn twee plaatsen die voorafgaand aan deze test kunnen worden uitgevoerd. Beide zorgen ervoor dat de voorwaarde het juiste resultaat oplevert. Dat zijn het statement 'i = 0' voorafgaand aan het while-statement en het statement 'i = i + 1' aan het einde van block dat wordt uitgevoerd als de voorwaarde True oplevert.

Variaties op het inlezen van een bestand

Ik vind het leuk om af en toe op een primitieve manier te programmeren. Met primitief bedoel ik dat enkel gebruik wordt gemaakt van if- en while-statements. Ik gebruik dus geen for-statements, en geen object-georiënteerde technieken. Dus geen classes. Een soort heimwee naar de taal Algol, de eerste computertaal waarin ik geprogrammeerd heb.

def lees_bestand():
    file_name = 'een_csv_bestand.csv'
    file_object = open(file_name)
    line = file_object.readline()
    print(line)
    while line != '':
        line = file_object.readline()
        print(line)
    file_object.close()
    
lees_bestand()

Als het input-bestand de volgende inhoud heeft

David;20240112;35.00
Jim;20240123;44.00
Ken;20240125;32.81
John;20240131;4.33

David;20240204;35.00
John;20240205;21.61
Ken;20240221;12.32
David;20240228;16.37

dan ziet de output er als volgt uit:

David;20240112;35.00

Jim;20240123;44.00

Ken;20240125;32.81

John;20240131;4.33



David;20240204;35.00

John;20240205;21.61

Ken;20240221;12.32

David;20240228;16.37


Ten opzichte van de input, worden er lege regels toegevoegd. Dat komt omdat er in het input-bestand aan het einde van elke regel een onzichtbaar nieuwe-regel-teken staat.
Als ik het bestand inlees als binary-bestand, met het volgende programma

input_file = open('py0051.csv', 'rb')
inhoud_bestand = input_file.read()
input_file.close
print(inhoud_bestand)

dan krijg ik op mijn Windows-computer als output
b'David;20240112;35.00\r\nJim;20240123;44.00\r\nKen;20240125;32.81\r\nJohn;20240131;4.33\r\n\r\nDavid;20240204;35.00\r\nJohn;20240205;21.61\r\nKen;20240221;12.32\r\nDavid;20240228;16.37\r\n'

De onzichtbare tekens \r en \n worden nu getoond. De tekenreeks begint met b gevolgd door een enkele quote (') en eindigt met een enkele quote. Dat wil zeggen dat het om een binary-weergave gaat. We gaan achterhalen welke binaire code er verscholen zit achter de tekens \r en \n.

x = b'\r'
print( ord(x) )         # 13
print( hex(ord(x)) )    # 0xd

x = b'\n'
print( ord(x) )         # 10
print( hex(ord(x)) )    # 0xa

Op internet kun je met de zoekterm 'ascii' vinden waar \r en \n voor staan:

 deci-
maal
hexa-
deci-
maal
 symbool  engels nederlands
 \r 13DCR carriage return  ga naar het begin van de regel 
 \n 10ALF line feed  ga naar de volgende regel 

Denk hierbij aan een ouderwetse mechanische typemachine waarbij je het papier omhoog moest draaien om naar de volgende regel te gaan (LF) en vervolgens de rol met papier naar rechts moest schuiven om aan het begin van de regel te gaan typen (CR). De afkorting ascii staat voor 'American Standard Code for Information Interchange'.

Hexadecimaal betekent 16-tallig. Dat betekent dat je een getal niet met de cijfers 0 t/m 9 weergeeft, maar ook A, B, C, D, E en F als cijfers beschouwt, met A=10, B=11, C=12, D=13, E=14, F =15. Dus: het decimale getal 13 komt overeen met het hexadecimale getal D, en het decimale getal 10 komt overeen met het hexadecimale getal A.

Hexadecimale en decimale representatie

def main():
    x = 'a'

    print( ord(x) )                     # 97
    print( x.encode('utf-8') )          # b'a'
    print( bytes(x, 'utf-8') )          # b'a'
    
    print( hex(ord(x)) )                # 0x61
    print( x.encode('utf-8').hex() )    # 61
    print( bytes(x, 'utf-8').hex() )    # 61

    n = 97

    print( chr(n) )                     # a

    b = b'\x61'

    print( str(b, 'utf-8') )            # a
    print( b.decode('utf-8') )          # a

if __name__ == '__main__':
    main()

Classes

Types

Gehele getallen

In Python kun je getallen gewoon optellen. Het programma

print(2 + 3)   # 5

heeft als output gewoon 5.

Variabelen

Maar als je in plaats van getallen letters intikt, bijvoorbeeld

print(abc + def)

dan interpreteert Python abc en def als namen van vaiabelen of als verboden tekencombinaties. Als die variabelen in het voorgaande niet dedefinieerd zijn, geeft Python een foutmelding. In dit geval wordt een syntax-error gemeld, omdat def een tekencombinatie is die je niet mag gebruiken, omdat def al gebruikt wordt om functies en methoden te definiëren.

Tekenreeksen

Wel kun je tekenreeksen abc en def 'optellen' als je aanhalingstekens om abc en def zet.

print('abc' + 'def')      # 'abcdef'

Het resultaat van dat 'optellen' is dat Python de tekenreeksen achter elkaar zet. Dat betekent dat het optellen van gehele getallen iets anders is dan optellen van tekenreeksen.

Verschillende betekenissen van +, −, * en /

Het programma

print( type(2) )           # <class 'int'>
print( type(3) )           # <class 'int'>
print( type(5) )           # <class 'int'>
print( type('abc') )       # <class 'str'>
print( type('def') )       # <class 'str'>

geeft aan dat 2, 3 en 5 van het type int zijn. int is een afkorting voor integer, dat is een geheel getal; str is de afkorting voor string, wat staat voor tekenreeks. Vandaar dat 2 + 3 een andere uitkomst geeft dan '2' + '3':

print(2 + 3)          # 5
print('2' + '3')      # '23'

Ook andere bewerkingen als aftrekken, delen en vermenigvuldigen, werken bij verschillende gegevenstypen net iets anders.

Drijvende-komma-getallen

Als je gaat delen, bijvoorbeeld

print( 5 / 2 )         # 2.5
print( 6 / 2 )         # 3.0

dan zie je dat de uitkomst een getal is waarin een decimale punt voorkomt, ook als de uitkomst van de deling een geheel getal is. Als je het type van zo'n getal met decimale komma opvraagt,

print( type(2.5) )           # <class 'float'>
print( type(3.0) )           # <class 'float'>

dan blijken 2.5 en 3.0 het type float te hebben, wat wil zeggen dat het drijvende-komma-getallen zijn. Drijvende-komma-getallen worden op een speciale manier opgeteld. Dat het een andere manier van optellen is als bij gewone gehele getallen, kun je zien in het volgende programma:

print( 1 + 1 + 1 - 3) / 10 )        # 0.0
print( 0.1 + 0.1 + 0.1 - 0.3) )     # 5.551115123125783e-17

e-17 staat voor 10-17. Beide optellingen zouden gelijk moeten zijn aan 0, maar door afrondingsverschillen is het antwoord bij het optellen van drijvende-komma-getallen bijna nul.

print( 1e-0 )        # 1.0
print( 1e-1 )        # 0.1
print( 1e-2 )        # 0.01
print( 1e-3 )        # 0.001

Het optellen en delen gaat bij drijvende-komma-getallen dus op een andere manier dan bij gehele getallen.

Decimale getallen

Python kent ook decimale getallen.

import decimal
print( decimal.Decimal(0.1) + 
       decimal.Decimal(0.1) + 
       decimal.Decimal(0.1) - 
       decimal.Decimal(0.3)   )   # Decimal(0.0)
print( type( decimal.Decimal(5.43) ) ) # <class 'decimal.Decimal'>

Bij decimale getallen kun je de precisie instellen.

import decimal
print( decimal.Decimal(1) / decimal.Decimal(7) )
    # 0.1428571428571428571428571429
decimal.getcontext().prec = 4
print( decimal.Decimal(1) / decimal.Decimal(7) )
    # 0.1429
print( type( decimal.Decimal(5.43) ) ) # <class 'decimal.Decimal'>

Complexe getallen

In Python worden getallen gevolgd door een j geïnterpreteerd als het imaginaire deel van een complex getal.

print( 2j + 3j )        # 5j
print( 2.0j + 3.0j )    # 5j
print( 2.1j + 3.2j )    # 5.300000000000001j

In de wiskunde wordt het complexe getal (0, 1) meestal aangegeven met de letter i, maar in de elektrotechniek, waar i gebruikt wordt voor stroomsterkte, wordt meestal de letter j gebruikt. Python volgt de notatie die gebruikelijk is in de elektrotechniek.

print( type( 5j ) )     # <class 'complex'>
print( type( 5.3j ) )   # <class 'complex'>

Breuken

Python kent ook breuken.

from fractions import Fraction
x = Fraction(3, 5)
y = Fraction(2, 5)
print(x)                  # 3/5
print(y)                  # 2/5
print(x + y)              # 1
print(x - y)              # 1/5
print( type(x) ) # <class 'fractions.Fraction'> print( type(x + y) ) # <class 'fractions.Fraction'>
print( Fraction('0.25') ) # Fraction(1, 4) print( Fraction('0.25') + Fraction('1.25') ) # Fraction(3, 2)

Voor breuken bestaan verschillende conversie-functies.

from fractions import Fraction
print(            (3.5).as_integer_ratio() )    # 7, 2
print( Fraction( *(3.5).as_integer_ratio() ) )  # 7/2
print( Fraction.from_float(1.75) ) # Fraction(7, 4)

Classes maken

Een lege class maken

We hebben hierboven gezien dat je van elke variabele de class kunt opvragen met behulp van het statement type(). Naast de classes die standaard in Python aanwezig zijn, kun je ook zelf classes maken. Dat gaat als volgt:

class MyClass:
    pass

obj = MyClass()     
print(MyClass)          # 
print(obj)              # <__main__.MyClass object at 0x0000024D8ED0A900>
print(type(MyClass))    # 
print(type(obj))        # 

Met het commando 'class MyClass' maak je een nieuwe class. Met 'pass' geef je aan dat het een class betreft, waar je dus nog bijna niets mee kunt doen. Het enige wat je ermee kunt doen is een object maken. Met 'obj = MyClass()' maak je een object met de naam obj van class MyClass. Vervolgens blijken MyClass en obj te bestaan, want je kunt ze printen. Vervolgens wordt met type gekeken wat de classes zijn van MyClass en obj. MyClass blijkt van de class 'type' te zijn.
Er zijn al wel een hoop methoden aanwezig. De methoden bij een object kun je opvragen met het commando dir().

class MyClass:
    pass

obj = MyClass() 
print('***** Methoden van MyClass *****')       
print(dir(MyClass))
print('***** Methoden van obj *****')       
print(dir(obj))

De output is:

***** Methoden van MyClass *****
['__class__', '__delattr__', '__dict__', '__dir__',
'__doc__', '__eq__', '__firstlineno__', '__format__',
'__ge__', '__getattribute__', '__getstate__', '__gt__',
'__hash__', '__init__', '__init_subclass__', '__le__',
'__lt__', '__module__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__static_attributes__', '__str__', '__subclasshook__',
'__weakref__'] ***** Methoden van obj ***** ['__class__', '__delattr__', '__dict__', '__dir__',
'__doc__', '__eq__', '__firstlineno__', '__format__',
'__ge__', '__getattribute__', '__getstate__', '__gt__',
'__hash__', '__init__', '__init_subclass__', '__le__',
'__lt__', '__module__', '__ne__', '__new__', '__reduce__',
'__reduce_ex__', '__repr__', '__setattr__', '__sizeof__',
'__static_attributes__', '__str__', '__subclasshook__',
'__weakref__']

instance-variables

Er miljoenen drijvende-komma-getallen denkbaar. Anders gezegd: Op basis van een class (bijvoorbeeld drijvende-komma-getallen) kun je een veelheid aan verschillende objecten (miljoenen drijvende-komma-getallen) creëren. Voorbeeld:

a = 2.3
b = 6.53
c = 21.43
pi = 3.14159
print( type(a),type(b)), type(c)), type(pi))

De getoonde variabelen a, b,c en pi nooemen we instance-variables van de class float. genoemd. Soms wordt dat naar het Nederlands vertaald vertaald met instantie-variabelen. Een instantie-variabele kan bij elk object weer een andere waarde hebben. Als je zelf een class maakt met het class-commando, kun je zo'n instance-varable direct aan een object koppelen. In onderstaand voorbeeld wordt de instance-variable straal gekoppeld aan de objecten cirkel_1 en cirkel_2::

class Cirkel:
    pass

cirkel_1 = Cirkel()
cirkel_1.straal = 4.774653    
cirkel_2 = Cirkel()
cirkel_2.straal = 3.183102    

print( 2 * 3.14159 * cirkel_1.straal)
                 # 30.000004236539997
print( 2 * 3.14159 * cirkel_2.straal)
                 # 20.00000282436

print( type( Cirkel ) )   
                    # <class 'type'>
print( type( cirkel_1 ) )
         # <class '__main__.Cirkel'>
print( type( cirkel_2 ) ) 
         # <class '__main__.Cirkel'>

Een nieuwe class maken met __init__()

Wat je vaker tegenkomt, is dat instance_variables via een method __init__() in de class-definitie aan het object wordt gekoppeld. Ook een berekening kan in de class-definitie worden opgenomen. Zo'n berekening wordt als functie aan de class gekoppeld. Een functie die aan een class is gekoppeld, wordt een method genoemd.

class Cirkel:
    def __init__(self, straal):
        self.straal = straal

    def omtrek(self):
        return  2 * 3.14159 * self.straal

cirkel_1 = Cirkel(4.774653)
cirkel_2 = Cirkel(3.183102)

print( cirkel_1.omtrek() )
                 # 30.000004236539997
print( cirkel_2.omtrek() )
                 # 20.00000282436

self verwijst naar het object. Hoewel __init__() twee variabelen kent, namelijk self en straal, hoef je in de commando's cirkel_1 = Cirkel(4.774653) en cirkel_2 = Cirkel(3.183102) bij class Cirkel maar één parameter (straal) mee te geven.

Class-variables

Een class-variabele is gekoppeld aan een class. Een class-variable is ook benaderbaar als er nog geen enkel object van de class is aangemaakt. Een class-variabele kan benaderd worden vauit alle objecten die op basis van die class zijn gemaakt.

import math

class Cirkel:
    pi = math.pi

    def __init__(self, straal):
        self.straal = straal

    def omtrek(self):
        return  2 * Cirkel.pi * self.straal

print( Cirkel.pi )      # 3.141592653589793

cirkel_1 = Cirkel(4.774653)
cirkel_2 = Cirkel(3.183102)

print( cirkel_1.pi )    # 3.141592653589793
print( cirkel_1.straal )         # 4.774653
print( cirkel_1.omtrek() )
                        # 30.00002957648093

print( cirkel_2.pi )    # 3.141592653589793
print( cirkel_2.straal )         # 3.183102
print( cirkel_2.omtrek() )
                       # 20.000019717653956

Alle objecten van een class doorlopen

Om alle objecten van een class te doorlopen, Maak je eerst een lege list als class_variabele aan. Als je een nieuw object aanmaakt, voeg je in het daarbij behorende __init__()-commando een verwijzing naar dat nieuwe object toe aan die list. De list bevat dan uiteindelijk verwijzingen naar alle eerder aangemaakte objecten. In het volgende voorbeeld zijn de objecten cirkels, die alle behoren tot de class Cirkel. In de volgende code gebruiken we self.__class__ voor de class waartoe de toe te voegen cirkel behoort.

import math

class Cirkel:
    pi = math.pi
    alle_cirkels = []

    def __init__(self, straal):
        self.straal = straal
        self.__class__.alle_cirkels.append(self)

print( Cirkel.alle_cirkels )
cirkel_1 = Cirkel(1)
print( Cirkel.alle_cirkels )
cirkel_2 = Cirkel(2)
print( Cirkel.alle_cirkels )
cirkel_3 = Cirkel(3)
print( Cirkel.alle_cirkels )

Static methods staan los van de class waar ze bij zijn ondergebracht

Je kunt bijvoorbeeld de som van de oppervlakten van de cirkels berekenen. In onderstaand voorbeeld wordt gebruik gemaakt van @staticmethod. Dat is een decorator die aangeeft dat de method die erop volgt, totaal_oppervlakte(), weliswaar is ondergebracht bij de class Cirkel, maar eigenlijk niets met die class te maken heeft. Een static method is aanwezig in een class omdat het op een of andere manier logisch is om de method daar onder te brengen. Er is wel een zekere functionaliteit die te maken heeft met de class, maar er hoeven geen objecten van de class aanwezig te zijn om de method uit te voeren. Een static method heeft geen parameter self of cls. Een static method is een method die gekoppeld is aan de class, maar niet aan een enkel object van de class. Vanuit een static method kunnen de variabelen die gedefinieerd zijn in de class, niet gewijzigd worden.

import math

class Cirkel:
    pi = math.pi
    alle_cirkels = []

    def __init__(self, straal):
        self.straal = straal
        self.__class__.alle_cirkels.append(self)

    def oppervlakte(self):
        return self.__class__.pi * self.straal * self.straal

    @staticmethod
    def totaal_oppervlakte():
        totaal = 0
        for c in Cirkel.alle_cirkels:
            totaal += c.oppervlakte()
        return totaal

cirkel_1 = Cirkel(1)
cirkel_2 = Cirkel(2)
cirkel_3 = Cirkel(3)
print( Cirkel.totaal_oppervlakte() )   # 43.982297150257104

Je roept een static method normaliter uit vanuit de class, niet vanuit een object. In het voorbeeld hierboven: Cirkel.totaal_oppervlakte(), en niet: cirkel_1.totaal_oppervlakte().

Class-methods

Class-methods zijn methods die niet aan een object zijn gekoppeld, maar aan een class. Het verschil met static methods houdt in dat (1) class-methods wel class_variabelen kunnen wijzigen en (2) in een class-method als eerste parameter een verwijzing naar de class zelf is opgenomen. Deze parameter wordt doorgaans niet self genoemd, maar cls. Een class-method moet worden voorafgegaan door de decorator @classmethod.

import math

class Cirkel:
    pi = math.pi
    alle_cirkels = []

    def __init__(self, straal):
        self.straal = straal
        self.__class__.alle_cirkels.append(self)

    def oppervlakte(self):
        return self.__class__.pi * self.straal * self.straal

    @classmethod
    def totaal_oppervlakte(cls):
        totaal = 0
        for c in Cirkel.alle_cirkels:
            totaal += c.oppervlakte()
        return totaal

print( Cirkel.totaal_oppervlakte() )

cirkel_1 = Cirkel(1)
cirkel_2 = Cirkel(2)
cirkel_3 = Cirkel(3)
print( Cirkel.totaal_oppervlakte() )

print('===')
print( cirkel_1.oppervlakte() )
print( cirkel_1.totaal_oppervlakte() )

Om de oppervlakte van cirkel één te weten te komen, moet je method cirkel_1.oppervlakte() uitvoeren, en niet cirkel_1.totaal_oppervlakte(), omdat totaal_oppervlakte een class-method is en de oppervlakte van alle aanwezige cirkels berekent, niet alleen die van cikel_1.

Properties

Geometrische vormen, zoals een cirkel, een rechthoek of een vierkant, hebben allemaal eeen oppervlakte. Om die oppervlaktes te berekenen, bestaan er formules. Oppervlakte kun je beschouwen als een eigenschap die bij de vorm hoort, of als het resultaat van een formule.
In onderstaand programma is de oppervlakte het resultaat van een method:

class Rechthoek:
    def __init__(self, hoogte, breedte):
        self.hoogte = hoogte
        self.breedte = breedte

    def oppervlakte(self):
        return self.hoogte * self.breedte

def main():
    rechthoekje = Rechthoek(2, 4)
    print( rechthoekje.oppervlakte() )     # 8

if __name__ == '__main__':
    main()

We gaan nu een aantal wijzigingen doorvoeren. (1) Allereerst vervangen we self.hoogte door self._hoogte en self.breedte door self._breedte. We plaatsen dus een underscore voor de variabele_namen. Hiermee geef je aan dat het niet is toegestaan dat deze variabelen van buitenaf worden gewijzigd. Met 'van buitenaf' wordt dan bedoeld 'van buiten de class-definitie'. (2) Vervolgens plaatsen we een regel met de tekst '@property' direct voorafgaand aan de regel 'def oppervlakte(self):'. (3) Als laatste vervangen we de aanroep 'rechthoekje.oppervlakte()' door 'rechthoekje.oppervlakte', d.w.z. dat we in de oproep de tekens () weglaten

class Rechthoek:
    def __init__(self, hoogte, breedte):
        self._hoogte = hoogte
        self._breedte = breedte

    @property
    def oppervlakte(self):
        return self._hoogte * self._breedte

def main():
    rechthoekje = Rechthoek(2, 4)
    print( rechthoekje.oppervlakte )     # 8

if __name__ == '__main__':
    main()

Met de decorator @property kun je dus wat door een method wordt berekend opvatten als een eigenschap van een object, dat niet zomaar gewijzigd mag worden. Wijziging mag alleen plaatsvinden vanuit een method van de class zelf.

class Rechthoek:
    def __init__(self, hoogte, breedte):
        self._hoogte = hoogte
        self._breedte = breedte

    @property
    def oppervlakte(self):
        return self._hoogte * self._breedte

def main():
    rechthoekje = Rechthoek(2, 4)
    print( rechthoekje.oppervlakte )     # 8

    rechthoekje.oppervlakte = 3    

if __name__ == '__main__':
    main()

heeft als output

8
Traceback (most recent call last): File "xxx.py", line 17, in main() File "xxx.py", line 14, in main rechthoekje.oppervlakte = 3 AttributeError: property 'oppervlakte' of 'Rechthoek' object has no setter

Setters

De conversie-formules voor graden Celsius en graden Fahrenheit zijn:

f = c * 9 / 5 + 32
c = (f - 32) * 5 / 9

We maken een class Temperatuur, waarin we de temperatuur opslaan in graden Celsius.

class Temperatuur:
    def __init__(self):
        self._celsius = 0

    @property
    def celsius_naar_fahrenheit(self):
        return self._celsius * 9 / 5 + 32

def main():
    temp = Temperatuur()
    print( temp._celsius , '\u00B0C')                   # 0 °C
    print( temp.celsius_naar_fahrenheit , '\u00B0F' )   # 32 °F

if __name__ == '__main__':
    main()

Omdat we ook andere temperaturen dan 0 °C willen kunnen weergeven, voegen we een setter toe aan de class Temperatuur.

class Temperatuur:
    def __init__(self):
        self._celsius = 0

    @property
    def celsius_naar_fahrenheit(self):
        return self._celsius * 9 / 5 + 32

    @celsius_naar_fahrenheit.setter
    def celsius_naar_fahrenheit(self, nieuwe_celsius):
        return nieuwe_celsius  * 9 / 5 + 32

def main():
    temp = Temperatuur()
    print( temp._celsius , '\u00B0C')                   # 0 °C
    print( temp.celsius_naar_fahrenheit , '\u00B0F' )   # 32 °F

    temp._celsius = 100

    print( temp._celsius , '\u00B0C')                   # 100 °C
    print( temp.celsius_naar_fahrenheit , '\u00B0F' )   # 212 °F

if __name__ == '__main__':
    main()

Vanuit functie main(), die niet gedefineerd is binnen de class Temperatuur, kunnen we toch de waarde van _celsius in object temp wijzigen.
Als we de naam celsius_naar_fahrenheit wijzigen in _fahrenheit, dan lijkt setter _fahrenheit op een variabele, die meeverandert met _celsius.

class Temperatuur:
    def __init__(self):
        self._celsius = 0

    @property
    def _fahrenheit(self):
        return self._celsius * 9 / 5 + 32

    @_fahrenheit.setter
    def _fahrenheit(self, nieuwe_celsius):
        return nieuwe_celsius  * 9 / 5 + 32

def main():
    temp = Temperatuur()
    print( temp._celsius , '\u00B0C')       # 0 °C
    print( temp._fahrenheit , '\u00B0F' )   # 32 °F

    temp._celsius = 100

    print( temp._celsius , '\u00B0C')       # 100 °C
    print( temp._fahrenheit , '\u00B0F' )   # 212 °F

if __name__ == '__main__':
    main()

Overigens, de regel 'return nieuwe_celsius * 9 / 5 + 32' mag je vervangen door 'self._fahrenheit = nieuwe_celsius * 9 / 5 + 32'.

Overerving ( Inheritance )

Het 'Liskov substitutie principe' betekent dat, als je super-class hebt waarvan een sub-class worrdt afgeleid, de sub-class dezelfde interface en implementatie moet erven van de super-class en dat objecten van de sub-class de objecten van de super-class kunnen vervangen. Anders gezegd: Het 'Liskov substitutie principe' zegt dat een afgeleid object - laten we dat Derived noemen - die erft van class - welke we Base noemen - het Base-object moet kunnen vervangen zonder de gewenste eigenschappen van een programma te veranderen.
Een voorbeeld: Veronderstel dat je denkt dat een vierkant een variant is van een rechthoek, en dat je de class Vierkant afleidt van de class Rechthoek. De eigenschap waar we dan onze aandacht op richten is oppervlakte, die is ondergebracht in de Base-class Rechthoek. In het programma hieronder ontstaat een fout, als je gebruik gaat maken van de functie resize(). De functie resize() kan gebruikt worden voor een object van de class Rechthoek(), maar niet voor een object van de class Vierkant.

class Rechthoek:
    def __init__(self, hoogte, breedte):
        self._hoogte = hoogte
        self._breedte = breedte

    @property
    def oppervlakte(self):
        return self._hoogte * self._breedte

    def resize(self, nieuwe_hoogte, nieuwe_breedte):
        self._hoogte = nieuwe_hoogte
        self._breedte = nieuwe_breedte

class Vierkant(Rechthoek):
    def __init__(self, zijde):
        super().__init__(zijde, zijde)

def main():
    rechthoekje = Rechthoek(2, 4)
    print( rechthoekje.oppervlakte )     # 8
    
    vierkantje = Vierkant(2)
    print( vierkantje.oppervlakte )      # 4

    rechthoekje.resize(3, 5)
    print( rechthoekje.oppervlakte )     # 15
    
    vierkantje.resize(3, 5)
    print( vierkantje.oppervlakte )      # 15     ????

if __name__ == '__main__':
    main()

De oorzaak van het feit dat je resize() niet kunt gebruiken voor een object van class Vierkant, is dat Rechthoek twee parameters vereist (hoogte en breedte) en Vierkant maar één (zijde). Vierkant heeft dus een andere interface als Rechthoek. Hoewel je een vierkant als een rechthoek kunt opvatten, kun je de class Vierkant beter niet programmeren als een subclass van de class Rechthoek, omdat dat heel gemakkelijk tot fouten kan leiden.

Een bestaande class uitbreiden met __new__()

Om iets zinnigs te kunnen doen met een class, kun je verschillende dingen doen. Eén daarvan is dat je een class kunt baseren op een andere class. Hieronder volgt een voorbeeld, waarin een eenheid aan de de class float wordt toegevoegd.

class FloatUnit(float):
    def __new__(cls, value, unit):
        instance = super().__new__(cls, value)
        instance.unit = unit
        instance.value = value
        return instance

    def __repr__(cls):
        return str(cls.value) + ' ' + str(cls.unit)

afstand = FloatUnit(1000, 'km')
print(afstand)             # 1000 km
print(type(afstand))       # <class '__main__.FloatUnit'>

De output van dit programma is:

1000 km
<class '__main__.FloatUnit'>

Je definieert eerst een methode __new__() die bij de class FloatUnit hoort. Deze wordt uitgevoerd onmiddellijk na het maken van de lege class. De methode super().__new__() hoort bij de class float. Deze methode zorgt ervoor, dat de class FloatUnit een attribuut met de naam instance krijgt, die een blauwdruk is voor een een float-waarde. In de daarop volgende commando's worden twee attributen aan deze blauwdruk toegevoegd, namelijk een waarde (value) en een eenheid (unit). In het return-commando wordt de attribuut instance gekoppeld aan de class FloatUnit. In dit voorbeeld volgt het print-commando wat je in de methode __repr__() hebt gedefinieerd.
In het volgende voorbeeld wordt naast __repr__() ook de methode __str__() gedefinieerd. Als __str__() gedefinieerd is, volgt print() de methode __str__() en niet de methode __repr__().

import decimal

class AmountOfMoney(decimal.Decimal):
    def __new__(cls, value, unit, sign):
        instance = super().__new__(cls, value)
        instance.unit = unit
        instance.value = value
        instance.sign = sign
        return instance

    def __str__(cls):
        return str(cls.sign) + ' ' + str(cls.value)
    
    def __repr__(cls):
        return str( cls.value) + ' ' + str(cls.unit)

geldbedrag = AmountOfMoney(1000, 'euro', '\N{EURO SIGN}')
print(type(geldbedrag))       # <class '__main__.AmountOfMoney'>
print(geldbedrag)             # € 1000
print(geldbedrag.__str__())   # € 1000
print(geldbedrag.__repr__())  # 1000 euro

Een class uitbreiden met __call__()

Aan een class kun je een method toevoegen met de naam __call__(). Als je een object van die class maakt, wordt de method __call__() niet uitgevoerd. Maar je kunt het object zelf uitvoeren. Iedere keer als je het object zelf aanroept op dezelfde manier als een method, wordt de methode __call__() uitgevoerd. Een class waarvoor een method __call__() is gedefinieerd, wordt een callable class genoemd. Voorbeeld:

class CallableClass:
    def __init__(self):
        self.counter = 0
        
    def __call__(self):
        self.counter += 1
        print( 'een callable class', self.counter )

def main():
    c = CallableClass()
    print( c.counter )     # 0
    print( callable(c) )   # True

    c()
    print( c.counter )     # 1

    c()
    print( c.counter )     # 2
    
if __name__ == '__main__':
    main()

De output is:

0
True
een callable class 1
1
een callable class 2
2

Iedere keer dat het object c wordt aangeroepen, wordt __call__(self) uitgeoverd, en wordt de teller die bij het object c hoort met 1 opgehoogd. Tussen de verschillende aanroepen door kun je bepaalde gegevens 'onthouden' en in een volgende aanroep opnieuw benaderen en gebruiken.

Composition, inheritance, polymorphism, encapsulation en abstraction

Hoe gebruik je alle constructies die de programmeertaal Python biedt? We lopen ze één voor één af.

Classes en objecten

In elk kledingstuk dat je in een winkel koopt is een label ingenaaid. Daarop staat op hoeveel graden je het kledingstuk kunt wassen, of je het kledingsuk in bleekwater mag leggen, of je het kledingstuk in de wasdroger mag drogen, of je het kledingstuk mag strijken, en wellicht ook van welk materiaal het kledingstuk is gemaakt (katoen, wol, zijde, ...). Dat is allemaal praktische informatie, die je nodig hebt bij het gebruik van een kledingstuk.

Het feit dat in elk commercieel verhandeld kledingstuk zo'n label is ingenaaid, doet vermoeden dat er regelgeving is, die voorschrijft dat zo'n label moet worden ingenaaid. Hoe dat juridisch precies geregeld is, is bij de meeste mensen niet bekend, maar om één of andere reden houden de kledingfabrikanten zich aan dit voorschrift.
Een class is te vergelijken met zo'n voorschrift. Een class beschrijft dus niet een concreet ding dat je kunt aanraken, maar eerder een wetmatigheid, een regel waar iedereen zich aan houdt. Een class beschrijft in de regel niet alle eigenschappen waaraan een object moet voldoen, maar alleen die eigenschappen die voor het programma van belang zijn.
Zoals een kledinglabel relevante kenmerken van een kledingstuk beschrijft, beschrijft een class de relevante eigeneschappen van een object. Een object is te vergelijken met een commercieel verhandeld kledingstuk met een label erin, met iets dat je kunt aanraken. Een object is te vergelijken met een concreet ding, dat aan de voorwaarden van het voorschrift voldoet.

Composition

Men spreekt over composition als twee classes met elkaar in verband staan, maar verder totaal verschillend zijn. Een voorbeeld is het verband tussen bibliotheken en boeken. Bibliotheken zijn totaal andere dingen dan boeken. Als je over een bibliotheek praat, denk je onmiddellijk aan verzameling boeken. Dat duidt dus op een verband tussen de bibliotheken en boeken. Dat verband kun je een naam geven, bijvoorbeeld 'is opgenomen in'. Je kunt zeggen : een boek is opgenomen in een bibliotheek.
Bij composition komen objecten van een zekere class voor in een andere class.

class Boek:
    def __init__(self, titel, auteur, 
                 uitgeverij, isbn):
        self.titel = titel
        self.auteur = auteur
        self.uitgeverij = uitgeverij
        self.isbn = isbn

    def get_info(self):
        return f"{self.titel} / {self.auteur}"

class Bibliotheek:
    def __init__(self, naam, boeken=None):
        self.naam = naam

        if boeken is None:
            self.boeken = []
        else:
            self.boeken = boeken

    def show_boeken(self):
        print(f"Alle boeken in {self.naam}:")
        for boek in self.boeken:
            info = boek.get_info()
            print(f"- {info}")
    
def main():
        boek_1 = Boek('Woordenboek',
                      'Van Dale',
                      'Van Dale Lexicografie',
                      '90-6648-427-6')
        boek_2 = Boek('Leerwoordenboek Nederlands',
                      'Marilene Gathier en Dorine de Kruyf',
                      'Coutinho', 
                      '978 90 6283 444 0')
        bibliotheek = Bibliotheek('Leuke boeken bibliotheek',
                                  [boek_1, boek_2])
        bibliotheek.show_boeken()

if __name__ == '__main__':
    main()        

Inheritance

Broeken, truien, sokken, jassen, petten, schoenen, enzovoorts, zijn allemaal kledingstukken. De vraag is: 'Wanneer noem je een voorwerp wel of niet een kledingstuk?' Bijvoorbeeld: Beschouw je een bril als kledingstuk? Zijn lenzen te beshouwen als kledingstuk? Beschouw je een oordopje als kledingstuk? De voorwaarden die jou doen besluiten of je iets wel of niet als een kledingstuk wil zien, die zullen van geval tot geval verschillen. Soms praat je over een bril als kledingstuk, soms praat je over een bril als medisch hulpmiddel. Bij inheritance gaat het over de voorwaarden die je in het programma gebruikt om te bepalen of iets wel of niet tot een class behoort. Als het programma kledingstukken betreft die je in een wasmachine kunt wassen, dan behoren schoenen, brillen, lenzen en oordopjes binnen dat programma duidelijk niet tot de kledingstukken, maar broeken, truien, sokken wel. En bij jassen en petten hangt het misschien af van wat voor jassen en petten het zijn.
Bij inheritance gaat het om twee soorten classes. Eén class is de bovenliggende class, die voor het algemene begrip staat, en in ons voorbeeld met 'kledingstuk' correspondeert. De andere classes zijn de onderliggende classes, die staan voor de verbijzonderingen, en in ons voorbeeld met broeken, truien, sokken enz. corresponderen. In een inheritance-relatie geldt dat

Polyphormism

Polyformisme is het verschijnsel dat sommige activiteiten dezelfde benaming hebben maar toch een heel anderssoortige actie inhouden. Je kunt het hebben over auto rijden, op de fiets rijden, paard rijden, in de trein rijden. Het wordt allemaal 'rijden' genoemd, maar als je het nader bekijkt, zijn het heel verschillende activiteiten.

Encapsulation

Bij encapsulation gaat het erom om gegevens en methoden die bij elkaar behoren onder te brengen in één omgeving en ervoor te zorgen dat deze gegevens en methoden niet van buiten die omgeving gewijzigd of aangeroepen kunnen worden. Dergelijke gegevens en methoden worden private gegevens en private methoden genoemd. Zo'n omgeving is dan meestal een class. Voor het onderbrengen van gegevens en methoden in een bepaalde class wordt in het engels vaak het woord wrapping gebruikt, wat omhullen betekent. De gegevens die van van buiten de class niet gewijzigd mogen worden, laat je in Python beginnen met een underscore ( _ ). Als er situaties zijn waarin een gegeven toch van buiten de class gewijzigd moeten kunnen worden, maak je daar binnen de class een methode voor, die setter wordt genoemd. Een methode die speciaal bedoeld is om een gegeven te raadplegen is een getter.

Abstraction

Bij abstraction gaat het erom de ingewikkelde details van een programma voor de gebruiker verborgen te houden. Aan de gebruiker laat men enkel datgene zien, wat voor hem of haar van belang is. Denk aan het dashbord van een auto. Hoe de motor precies werkt, is voor de bestuurder niet van belang. Op het dashbord ziet de bestuurder alleen de gegevens die voor hem van belang zijn.

Aan de slag met classes

Nu volgen een aantal programmaatjes die laten zien hoe classes gebruikt kunnen worden.

Een attribuut van een object wijzigen

In onderstaand programma wordt het attribuut n op twee manieren gewijzigd. Bij het maken van object a krijgt attribuut n de waarde 5, bij het uitvoeren van methode __init__(5). Via het statement 'a.n = 3' krijgt attribuut n de waarde 3 van buiten het object a. Via het statement 'a.ophogen_n()' krijgt attribuut n de waarde 4, maar nu vanuit een methode van object a zelf.

class A():
    def __init__(self, n):
        self.n = n
        
    def ophogen_n(self):
        self.n = self.n + 1
        
def main():
    a = A(5)
    print(a.n)       # 5
    a.n = 3
    print(a.n)       # 3
    a.ophogen_n()
    print(a.n)       # 4

main()

Aan een attribuut van type list een item toevoegen

In het volgende voorbeeld bevat de class Bibliotheek de list lijst_boeken als attribuut. Deze list wordt met 3 boeken gevuld.

class Bibliotheek():
    def __init__(self,):
        self.lijst_boeken = []

    def voeg_boek_toe_aan_lijst(self, boek):
        self.lijst_boeken.append(boek)

    def print_boeken(self):
        for idx, item in enumerate(self.lijst_boeken):
            print(idx, item.titel)
        print( '---')
    
class Boek():
    def __init__(self, titel, auteur, uitgeverij, isbn):
        self.titel = titel
        self.auteur = auteur
        self.uitgeverij = uitgeverij
        self.isbn = isbn

def main():
    bieb = Bibliotheek()
    boek_1 = Boek('Jip en Janneke', 'Annie M.G. Schmidt',
                  'Querido', 'ISBN 978 90 451 0225 2')
    boek_2 = Boek('Pippi Langkous', 'Astrid Lindgren',
                  'Ploegsma', '978 90 216 8023 1')
    boek_3 = Boek('De zoete zusjes helpen de natuur',
                  'Hanneke de Zoete', 'Kosmos',
                  '978 90 439 9384 2')
    bieb.print_boeken()
    bieb.lijst_boeken.append(boek_1)
    bieb.print_boeken()
    bieb.lijst_boeken.append(boek_2)
    bieb.print_boeken()
    bieb.lijst_boeken.append(boek_3)
    bieb.print_boeken()

main()

De output is

---
0 Jip en Janneke
---
0 Jip en Janneke
1 Pippi Langkous
---
0 Jip en Janneke
1 Pippi Langkous
2 De zoete zusjes helpen de natuur
---

Van een attribuut van type list het laatste item ophalen

Je wilt uit een attribuut van type list (self.collectie) het laatste item ophalen.

class Item():
    def __init__(self, item):
        self.item = item

class Collectie():
    def __init__(self,):
        self.collectie = [Item('item_1'),
                          Item('item_2'), 
                          Item('item_3')]

    def get_laatste_item(self):
        return self.collectie[-1].item

def main():
    collectie = Collectie()
    laatste_item = collectie.get_laatste_item()
    print( laatste_item )              # item_3

main()

Via een keten van objecten een attribuut benaderen

In onderstaand voorbeeld halen via de class Company de naam van de eigenaar van een bedrijf op.

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        
class Company:
    def __init__(self, name, owner):
        self.name = name
        self.owner = owner
        
def main():
    john = Person('John', 22)
    bedrijf = Company('Bedrijf', john)
    print( bedrijf.owner.name,
           bedrijf.owner.age,
           sep = ' / ' )        # John / 22

main()

Via een keten van objecten een methode uitvoeren

Via een keten van objecten kun je een methode die bij een andere class behoort, uitvoeren. Stel je voor dat A een motor voorstelt, en B een onderdeel van die motor. C stelt een onderdeel van B voor en D is weer een onderdeel van C. Je wilt vanuit class A een onderdeeltje D aansturen. In onderstaand voorbeeld maakt het uitvoeren van methode do_a() van een object van type A, dat onderdeeltje d van type D de actie do_d() uitvoert.

class A():
    def do_a(self):
        b = B()
        b.do_b()
        
class B():
    def do_b(self):
        c = C()
        c.do_c()

class C():
    def do_c(self):
        d = D()
        d.do_d()

class D():
    def do_d(self):
        print( 'd.do_d' )     # d.do_d

def main():
    a = A()
    a.do_a()
    
if __name__ == '__main__':
    main()

Json

Json-standaard

Standaard JSON kent (1) geen commentaar, (2) geen komma's waarna niets volgt en (3) geen enkele aanhalingstekens bij strings.
Het omzetten van gegevens naar het JSON-formaat wordt serialization genoemd. Het tegenovergestelde proces, deserialization, houdt in dat gegevens in JSON-formaat wordt omgezet in een in Python-gegevenstype.

Van Python naar Json ( json.dumps )

De volgende Python-code zet de python-gegevenstypen om in strings met json-code.

import json

def python_to_json():
    print(json.dumps({"naam": "Jan", "leeftijd": 31})) # dictionary -> json-object
    print(json.dumps(["appel", "banaan"]))             # list       -> json-array
    print(json.dumps(("appel", "banaan")))             # tuple      -> json-array
    print(json.dumps("hallo"))                         # string     -> json-string
    print(json.dumps(56))                              # integer    -> json-number
    print(json.dumps(41.87))                           # float      -> json-number
    print(json.dumps(True))                            # True       -> json-true
    print(json.dumps(False))                           # False      -> json-false
    print(json.dumps(None))                            # None       -> json-null

python_to_json()

Het programma heeft als output:

{"naam": "Jan", "leeftijd": 31}
["appel", "banaan"]
["appel", "banaan"]
"hallo"
56
41.87
true
false
null

Van Json naar Python ( json.loads )

De volgende Python-code zet json-gegevens om naar python-gegevenstypen .

import json

def json_to_python():
    print(json.loads('{"naam": "Jan", "leeftijd": 31}')) # json-object -> dictionary
    print(json.loads('["appel", "banaan"]'))             # json-array  -> list
    print(json.loads('"hallo"'))                         # json-string -> string
    print(json.loads('56'))                              # json-number -> integer
    print(json.loads('41.87'))                           # json-number -> float
    print(json.loads('true'))                            # json-true   -> True
    print(json.loads('false'))                           # json-false  -> False
    print(json.loads('null'))                            # json-null   -> None

json_to_python()

Het programma heeft als output:

{'naam': 'Jan', 'leeftijd': 31}
['appel', 'banaan']
hallo
56
41.87
True
False
None

Inlezen json-file ( json.load )

Als input-file nemen we infile.json met de volgende inhoud:

[ {"naam": "Jan", "leeftijd": 31},
  ["appel", "banaan"],
  "hallo",
  56,
  41.87,
  true,
  false,
  null
]

Het Python-programma om het json-bestand in te lezen, ziet er als volgt uit:

import json

def read_json_file():
    with open("infile.json", mode="r", encoding="utf-8") as jsonfile:
        infile_data = json.load(jsonfile)
    print(infile_data)

read_json_file()

Als output krijgen we:

[{'naam': 'Jan', 'leeftijd': 31}, ['appel', 'banaan'], 
'hallo', 56, 41.87, True, False, None]

Wegschrijven json-file ( json.dump )

Een list wegschrijven als json-file
import json

def write_json_file():
    python_list = [{'naam': 'Jan', 'leeftijd': 31}, ['appel', 'banaan'], 
            'hallo', 56, 41.87, True, False, None]
    with open("outfile.json", mode="w", encoding="utf-8") as jsonfile:
        json.dump(python_list, jsonfile)

write_json_file()

De inhoud van het weggeschreven bestand is:

[{"naam": "Jan", "leeftijd": 31}, ["appel", "banaan"], "hallo", 
56, 41.87, true, false, null]
Een dictionary wegschrijven als json-file
import json

def write_json_file():
    python_dict = { 'naam': 'Jan',
                    'leeftijd': 31, 
                    'woonplaats': 'Rotterdam'
                   } 
    with open("outfile.json", mode="w", encoding="utf-8") as jsonfile:
        json.dump(python_dict, jsonfile)

write_json_file()
            

De inhoud van het weggeschreven bestand is:

{"naam": "Jan", "leeftijd": 31, "woonplaats": "Rotterdam"}

In dit voorbeeld werd een string als key gebruikt. Er zijn een drietal gegevenstypen die in Json niet als key mogen worden gebruikt. Dat zijn dict, list en tuple. Overzicht:

Python data type   Toegestaan als JSON key
dict 
list 
tuple 
str 
int 
float 
bool 
None 

Formatteren Json-code

Als input-file nemen we rechthoeken.json met de volgende inhoud:

[ { "illustration_upper_left_x": 0,
    "illustration_upper_left_y": 0,
    "element_x": 10,
    "element_y": 5, 
    "element_width": 300,
    "element_height": 30,
    "element_top": 10,
    "element_right": 10,
    "element_bottom": 10,
    "element_color": "lightGreen",
    "text_x": 15,
    "text_y": 24,
    "text_length": 100,
    "text": "the quick brown fox jumps over the lazy dog"
  },
  { "illustration_upper_left_x": 0,
    "illustration_upper_left_y": 0,
    "element_x": 10,
    "element_y": 5, 
    "element_width": 300,
    "element_height": 30,
    "element_top": 10,
    "element_right": 10,
    "element_bottom": 10,
    "element_color": "lightGreen",
    "text_x": 15,
    "text_y": 24,
    "text_length": 100,
    "text": "filmquiz bracht knappe ex-yogi van de wijs"
  },
  { "illustration_upper_left_x": 0,
    "illustration_upper_left_y": 0,
    "element_x": 10,
    "element_y": 5, 
    "element_width": 300,
    "element_height": 30,
    "element_top": 10,
    "element_right": 10,
    "element_bottom": 10,
    "element_color": "lightGreen",
    "text_x": 15,
    "text_y": 24,
    "text_length": 100,
    "text": { "text_1": "Als beginnend concertist debuteerde een ",
              "text_2": "fijngevoelige gitarist, hierna improviseerden",
              "text_3": "jeugdige klankkunstenaars levendig maar",
              "text_4": "notenblind op Peruviaanse quena's, robuuste",
              "text_5": "slagwerkers trommelden uitzinnige volksmuziek,",
              "text_6": "waarna xylofonisten 'Yesterday' zongen.; "
            }
  }
]

Het Python-programma om het json-bestand rechthoeken.json in te lezen, ziet er als volgt uit:

import json

def read_json_file():
    with open("rechthoeken.json", mode="r", encoding="utf-8") as jsonfile:
        infile_data = json.load(jsonfile)
    print(infile_data)

read_json_file()

De outputfile bestaat uit één regel waarin de gehele list is opgenomen.

[{'illustration_upper_left_x': 0, 'illustration_upper_left_y': 0, 'element_x': 10, 'element_y': 5, 'element_width': 300, 'element_height': 30, 'element_top': 10, 'element_right': 10, 'element_bottom': 10, 'element_color': 'lightGreen', 'text_x': 15, 'text_y': 24, 'text_length': 100, 'text': 'the quick brown fox jumps over the lazy dog'}, {'illustration_upper_left_x': 0, 'illustration_upper_left_y': 0, 'element_x': 10, 'element_y': 5, 'element_width': 300, 'element_height': 30, 'element_top': 10, 'element_right': 10, 'element_bottom': 10, 'element_color': 'lightGreen', 'text_x': 15, 'text_y': 24, 'text_length': 100, 'text': 'filmquiz bracht knappe ex-yogi van de wijs'}, {'illustration_upper_left_x': 0, 'illustration_upper_left_y': 0, 'element_x': 10, 'element_y': 5, 'element_width': 300, 'element_height': 30, 'element_top': 10, 'element_right': 10, 'element_bottom': 10, 'element_color': 'lightGreen', 'text_x': 15, 'text_y': 24, 'text_length': 100, 'text': {'text_1': 'Als beginnend concertist debuteerde een ', 'text_2': 'fijngevoelige gitarist, hierna improviseerden', 'text_3': 'jeugdige klankkunstenaars levendig maar', 'text_4': "notenblind op Peruviaanse quena's, robuuste", 'text_5': 'slagwerkers trommelden uitzinnige volksmuziek,', 'text_6': "waarna xylofonisten 'Yesterday' zongen.; "}}]

Als je binnen je programma een beter beeld wilt hebben van wat je hebt ingelezen, kun je de ingelezen data formatteren. Bijvoorbeeld:

import json

def read_json_file():
    with open("rechthoeken.json", mode="r", encoding="utf-8") as jsonfile:
        infile_data = json.load(jsonfile)
        formatted_data = json.dumps(infile_data, 
                                    indent=4, separators=(". ", " = "))
        print(formatted_data)
        
read_json_file()

De output wordt dan als volgt geformatteerd:

[
    {
        "illustration_upper_left_x" = 0. 
        "illustration_upper_left_y" = 0. 
        "element_x" = 10. 
        "element_y" = 5. 
        "element_width" = 300. 
        "element_height" = 30. 
        "element_top" = 10. 
        "element_right" = 10. 
        "element_bottom" = 10. 
        "element_color" = "lightGreen". 
        "text_x" = 15. 
        "text_y" = 24. 
        "text_length" = 100. 
        "text" = "the quick brown fox jumps over the lazy dog"
    }. 
    {
        "illustration_upper_left_x" = 0. 
        "illustration_upper_left_y" = 0. 
        "element_x" = 10. 
        "element_y" = 5. 
        "element_width" = 300. 
        "element_height" = 30. 
        "element_top" = 10. 
        "element_right" = 10. 
        "element_bottom" = 10. 
        "element_color" = "lightGreen". 
        "text_x" = 15. 
        "text_y" = 24. 
        "text_length" = 100. 
        "text" = "filmquiz bracht knappe ex-yogi van de wijs"
    }. 
    {
        "illustration_upper_left_x" = 0. 
        "illustration_upper_left_y" = 0. 
        "element_x" = 10. 
        "element_y" = 5. 
        "element_width" = 300. 
        "element_height" = 30. 
        "element_top" = 10. 
        "element_right" = 10. 
        "element_bottom" = 10. 
        "element_color" = "lightGreen". 
        "text_x" = 15. 
        "text_y" = 24. 
        "text_length" = 100. 
        "text" = {
            "text_1" = "Als beginnend concertist debuteerde een ". 
            "text_2" = "fijngevoelige gitarist, hierna improviseerden". 
            "text_3" = "jeugdige klankkunstenaars levendig maar". 
            "text_4" = "notenblind op Peruviaanse quena's, robuuste". 
            "text_5" = "slagwerkers trommelden uitzinnige volksmuziek,". 
            "text_6" = "waarna xylofonisten 'Yesterday' zongen.; "
        }
    }
]

Comprimeren

In Python creëren we een dictionary, die we op twee manieren omzetten naar json-formaat. De tweede manier bevat minder spaties, en is daardoor korter.

import json

def create_json():
    # creëer dictionary json_dict
    json_dict = { 1: { "illustration_upper_left_x": 0,
                           "illustration_upper_left_y": 0,
                          "element_x": 10,
                          "element_y": 5, 
                          "element_width": 300,
                          "element_height": 30,
                          "element_top": 10,
                          "element_right": 10,
                          "element_bottom": 10,
                          "element_color": "lightGreen",
                          "text_x": 15,
                          "text_y": 24,
                          "text_length": 100,
                          "text": "the quick brown fox jumps over the lazy dog"
                        },
                  2: { "illustration_upper_left_x": 0,
                          "illustration_upper_left_y": 0,
                          "element_x": 10,
                          "element_y": 5, 
                          "element_width": 300,
                          "element_height": 30,
                          "element_top": 10,
                          "element_right": 10,
                          "element_bottom": 10,
                          "element_color": "lightGreen",
                          "text_x": 15,
                          "text_y": 24,
                          "text_length": 100,
                          "text": "filmquiz bracht knappe ex-yogi van de wijs"
                        }
                  }
    # zet dictionary json_dict om in string json_data
    json_data = json.dumps(json_dict)
    print('aantal bytes in json_data :', len(json_data))

    # schrijf json_data weg als bestand outfile1.json
    with open("outfile1.json", mode="w", encoding="utf-8") as output_file:
        output_file.write(json_data)
        
    # zet dictionary json_dict om in string mini_json
    mini_json = json.dumps(json_dict, indent=None, separators=(",", ":"))
    print('aantal bytes in mini_json :', len(mini_json))
    
    # schrijf mini_jason weg als bestand outfile2.json
    with open("outfile2.json", mode="w", encoding="utf-8") as output_file:
        output_file.write(mini_json)

create_json()

De output is:.

aantal bytes in json_data : 687
aantal bytes in mini_json : 630

Een csv-bestand omzetten naar een json-bestand

De volgende python-code zet het csv-bestand 'infile.csv' om naar het json-bestand 'outfile.json':

import csv 
import json

def csv_to_json(csvFilePath, jsonFilePath):
    jsonArray = []
    try:
        with open(csvFilePath, encoding='utf-8') as csvf: 
            csvReader = csv.DictReader(csvf)
            for row in csvReader: 
                jsonArray.append(row)
                
        with open(jsonFilePath, 'w', encoding='utf-8') as jsonf: 
            jsonString = json.dumps(jsonArray, indent=4)
            jsonf.write(jsonString)
    except FileNotFoundError:
        print("csv_to_json: csv-input-file niet gevonden " + csvFilePath)

def main():
    csvFilePath = r'infile.csv'
    jsonFilePath = r'outfile.json'
    csv_to_json(csvFilePath, jsonFilePath)
         
if __name__ == '__main__':
    main()

Type hints

variabele

Een gewone manier om een variabele te definiëren is :

a = 15
print(a)

De output is:

15

Je kunt specificeren welk type de variabele moet hebben :

a: int = 15
print(a)

De output blijft:

15

Intern in Python wordt in de directory __annotations__ de type-hint opgeslagen:

a: int = 15
print(a)
print(__annotations__)

De output wordt:

15
{'a': <class 'int'>}

De python-interpreter doet helemaal niets met de toevoeging van ': int'. Het is meer bedoeld als herinnering voor de programmeur, in de trant van 'het is de bedoeling dat een variabele met de naam a van het type int is. ' Als een programmeur zich niet aan zo'n voornemen houdt, genereert de python-interpreter geen errors of warnings. Het volgende programma kent de waarde 15.72 van het type float toe aan variabele a, maar specificeert dat a een integer-waarde zou moeten hebben. Niettemin wordt er bij uitvoeriing van het programma geen waarschuwing gegenereerd:

a: int = 15.72
print(a)
print(__annotations__)

De output is:

15.72
{'a': <class 'int'>}

Toevoegingen als ': int' of ': float' worden 'type hints' genoemd Sommige editors (PyCharm) en programma's van externe partijen (MyPy) gebruiken type hints om inconsistenties in programma's op te sporen.

int, float, list, tuple, dict, bool, None
s: str = "abcd"
i: int = 15
f: float = 41.25
l: list = [ "a", "b" ]
t: tuple = ( "a", "b" )
d: dict = { 1 : "a" }
b: bool = True
n: None = None

print(s)
print(i)
print(f)
print(l)
print(t)
print(d)
print(b)
print(n)
print('-----')
print(__annotations__)

De output is:

abcd
15
41.25
['a', 'b']
('a', 'b')
{1: 'a'}
True
None
-----
{'s': <class 'str'>, 'i': <class 'int'>, 'f': <class 'float'>, 'l': <class 'list'>, 't': <class 'tuple'>, 'd': <class 'dict'>, 'b': <class 'bool'>, 'n': None}

In het volgende programma worden drie functies zonder type hints gedefinieerd:

def func_1(var): 
    return print(var)

def func_2(var = "x"): 
    return print(var)

def func_3(var = "y"):
    print(var) 
    return var

func_1("a")
func_2()
func_2("b")
func_3()
func_3("c")

Bij func_2 is de default-waarde voor variabele var gelijkgesteld aan "x". Bij func_3 is deze default-waarde gelijkgesteld aan "y". De output is:

a
x
b
y
c

Je kunt aan deze functies type-hints toevoegen De type-hint voor de return-waarde van de functie geeft je aan met '->':

def func_1(var: str): 
    return print(var)

def func_2(var: str = "x"): 
    return print(var)

def func_3(var: str = "y") -> str: 
    print(var) 
    return var
 
func_1("a")
func_2()
func_2("b")
func_3()
func_3("c")

print('-----')
print(__annotations__)
print(func_1.__annotations__)
print(func_2.__annotations__)
print(func_3.__annotations__)

De output wordt

a
x
b
y
c
-----
{}
{'var': <class 'str'>}
{'var': <class 'str'>}
{'var': <class 'str'>, 'return': <class 'str'>}

De volgende code definieert een list, een tuple en een dictionary:

namen = ["Jan", "Piet", "Kees"]
versies = (3, 7, 2)
opties = {"gecentreerd": False, "met_hoofdletters": True}

print(namen[0], namen[1], namen[2])
print(versies[0], versies[1], versies[2])
print(opties["gecentreerd"], opties["met_hoofdletters"])

De output is:

Jan Piet Kees
3 7 2
False True

Je kunt aan deze code type hints voor de individuele objecten in de list toevoegen. Je moet daarvoor wel delen van de standaard-module typing importeren.

from typing import Dict, List, Tuple

namen: List[str] = ["Jan", "Piet", "Kees"]
versies: Tuple[int, int, int] = (3, 7, 1)
opties: Dict[str, bool] = {"gecentreerd": False, "met_hoofdletters": True}

print(namen[0], namen[1], namen[2])
print(versies[0], versies[1], versies[2])
print(opties["gecentreerd"], opties["met_hoofdletters"])

print('-----')
print(__annotations__)

De output wordt:

Jan Piet Kees
3 7 2
False True
-----
{'namen': typing.List[str], 'versies': typing.Tuple[int, int, int], 'opties': typing.Dict[str, bool]}

We gaan uit van een programma waarin een class en een object worden gedefinieerd.

import math

class Mal():
    pi = math.pi

    def __init__(self, radius):
        self.radius = radius

    def omtrek_en_oppervlakte(self):
        self.omtrek = 2 * self.pi * self.radius
        self.oppervlakte = self.pi * self.radius * self.radius
        return self.omtrek, self.oppervlakte
        
def main():
    object_1 = Mal(10)
    (omtrek, oppervlakte) = object_1.omtrek_en_oppervlakte()
    print(omtrek, oppervlakte)

main()

De output wordt:

62.83185307179586 314.1592653589793
=====

Aan het programma voegen we type-hints toe.

import math

class Circle_template():
    print('----- Circle_template() ----- class definiton -----')
    pi: float = math.pi
    print('Circle_template: __annotations__ =', __annotations__)

    def __init__(self, radius):
        print('-----__init__(self, radius) ----- method')
        self.radius: float = radius
        print('__init__(self, radius): self.__annotations__ ==', self.__annotations__)

    def omtrek_en_oppervlakte(self):
        print('----- omtrek_en_oppervlakte(self) ----- method')
        self.omtrek: float = 2 * self.pi * self.radius
        self.oppervlakte: float = self.pi * self.radius * self.radius
        print('omtrek_en_oppervlakte(self): __annotations__ =', __annotations__)
        print('omtrek_en_oppervlakte(self): self.__annotations__ ==', self.__annotations__)
        return self.omtrek, self.oppervlakte
        
def main():
    print('----- main() ----- function')
    circle = Circle_template(10)
    print( 'main(): circle.__annotations__ ==',  circle.__annotations__)
    (omtrek, oppervlakte) =  circle.omtrek_en_oppervlakte()
    print(omtrek, oppervlakte)

print('----- program -----')
main()

De output wordt:

------ Circle_template() ----- class definiton -----
Circle_template: __annotations__ = {'pi': }
----- program -----
----- main() ----- function
-----__init__(self, radius) ----- method
__init__(self, radius): self.__annotations__ == {'pi': }
main(): circle.__annotations__ == {'pi': }
----- omtrek_en_oppervlakte(self) ----- method
omtrek_en_oppervlakte(self): __annotations__ = {}
omtrek_en_oppervlakte(self): self.__annotations__ == {'pi': }
62.83185307179586 314.1592653589793

Wat opvalt is dat omtrek en oppervlakte niet worden opgenomen in de annotations. Dit schijnt te maken te hebben met het feit dat een class een soort mal is, een template, dat als het gedefinieerd wordt, nog geen object is. Dit is voor mij vooralsnog onbekend terrein en ik ga er verder niet op in.

Refactoring

Refactoring gaat over het herschrijven van bestaande programmatuur. Het gaat erom om onduidelijke of onnodig gecompliceerde code om te zetten in goede code, zó, dat het programma precies blijft doen wat het altijd al deed. Waarom zou je bestaande programma's willen herschrijven? Daarvoor zijn verschillende redenen te bedenken:

In het boek 'Five lines of code' van Christian Clausen (Manning 2021) worden een aantal regels genoemd, die programma-code kunnen verbeteren. Cristian Clausen schrijft dat deze regels zijn bedoeld als zij-wieltjes. Met andere woorden: als hulpmiddel voor een beginner. Je mag dus afwijken van deze regels, als je daar goede redenen voor hebt. De regels zijn: Als je bestaande code wilt omzetten naar nieuwe volgens bovenstaande regels, heb je daarvoor zg. refactoring patterns:

Vijf statements in een functie

In het volgende programma voldoet de functie containsEven() aan de regel, dat je maar vijf statements in een functie mag hebben, maar de functie minimum() niet. De functie containsEven(arr) checkt of de twee-dimensionale array arr een even getal bevat. De functie minimum(arr) bepaalt het laagste getal in de twee-dimensionale array arr.

def containsEven(arr):
    for x in arr:
        for y in x:
            if y % 2 == 0:
                return True
    return False

def minimum(arr):
    r = (9**9)**9
    for x in arr:
        for y in x:
            if y < r:
                r = y
    return r
   
def main():
    arr = [ [21, 23, 67],
            [35, 67, 33],
            [41, 58, 89],
            [23, 11, 73],
          ]
    r = containsEven(arr)
    print(r)
    r = minimum(arr)
    print(r)

main()

De output wordt:

True
11

Als je er niet op let worden functies of methoden al maar langer. Daarmee wordt het lastiger ze te begrijpen.

Deel functies en methoden op in kleinere functies en methoden

Een hulpmiddel om te komen tot "five lines" is het refactoring-patroon "extract method. Deze passen we toe op bovenstaande functie minimum().

  1. Als een functie te lang is, probeer deze dan op te delen in stukken die bij elkaar horen.
  2. Scheidt die stukken door er blanco regels tussen te plaatsen. Eventueel kun je commentaar-regels tussenvoegen.
    In onderstaande programmacode wordt één stuk tussen twee commentaarregels geplaatst.
    def minimum(arr):
        r = (9**9)**9
        for x in arr:
            for y in x:
                #--------
                if y < r:
                    r = y
                #--------
        return r
       
    def main():
        arr = [ [21, 23, 67],
                [35, 67, 33],
                [41, 58, 89],
                [23, 11, 73],
              ]
        r = minimum(arr)
        print(r)
    
    main()
  3. Verplaats elk stuk naar een nieuwe functie (1).
    def minimum(arr):
        r = (9**9)**9
        for x in arr:
            for y in x:
                #--------
                #--------
        return r
    #--------
    def min():
        if y < r:
            r = y
    #--------  
    def main():
        arr = [ [21, 23, 67],
                [35, 67, 33],
                [41, 58, 89],
                [23, 11, 73],
              ]
        r = minimum(arr)
        print(r)
    
    main()
  4. Voeg parameters en return-waarden toe aan de functie. Roep de functie aan op de plaats waar de code zich oorspronkelijk bevond.
    def minimum(arr):
        r = (9**9)**9
        for x in arr:
            for y in x:
                #--------
                r = min(r, arr, x, y)
                #--------
        return r
    #--------
    def min(r, arr, x, y):
        if y < r:
            r = y
        return r
    #--------
    def main():
        arr = [ [21, 23, 67],
                [35, 67, 33],
                [41, 58, 89],
                [23, 11, 73],
              ]
        r = minimum(arr)
        print(r)
    
    main()
  5. Iedere keer als je een stukje code hebt opgenomen in een nieuwe functie, kijk je of er fouten in het programma zijn ontstaan, bijvoorbeeld door in IDLE te kiezen voor Run->Check Module (Alt-X). Bij een andere taal zou je kunnen compileren.
  6. Los de fouten die gerapporteerd worden op.
  7. Tenslotte verwijder je de blanco regels en het overbodig geworden commentaar.
    def minimum(arr):
        r = (9**9)**9
        for x in arr:
            for y in x:
                r = min(r, arr, x, y)
        return r
    
    def min(r, arr, x, y):
        if y < r:
            r = y
        return r
    
    def main():
        arr = [ [21, 23, 67],
                [35, 67, 33],
                [41, 58, 89],
                [23, 11, 73],
              ]
        r = minimum(arr)
        print(r)
    
    main()

ofwel een functie aanroepen, ofwel een functie meegeven als parameter, maar niet beide

Een korte vertaling van de regel "either call or pass' zou kunnen zijn: "ofwel aanroepen, ofwel doorgeven, maar niet beide". Een functie zou dus ofwel methoden behorende bij een object moeten aanroepen, ofwel het object doorgeven als een argument, maar niet beide. Als je het voorbeeld dat daarbij in het boek "Five lines of code" wordt gegeven, vertaalt naar Python, krijg je zoiets als

def average(arr):
    return sum(arr) / arr.__len__()

def main():
    arr = [21, 23, 67, 35, 67, 33,
           41, 58, 89, 23, 11, 84]
    print(average(arr))

main()

In sum(arr) wordt het object arr als argument doorgegeven aan de functie sum(), maar in arr.__len__() wordt een method van het object aangeroepen. In Python is dat nogal een gekunsteld voorbeeld, want arr.__len__() is een heel ongewone schrijfwijze van len(arr). Volgens de regel "either pass or call" zou je dit programma moeten verbeteren tot

def average(arr):
    return sum(arr) / len(arr)

def main():
    arr = [21, 23, 67, 35, 67, 33,
           41, 58, 89, 23, 11, 84]
    print(average(arr))

main()

De reden van de regel "either call or pass" wordt uitgelegd als: Elk statement in een functie zou hetzelfde abstractie-niveau moeten hebben. Het doorgeven van een object als argument leidt in de regel tot een hoger abstractie-niveau dan het aanroepen van een method van een object. Vandaar dat je binnen een functie moet kiezen voor het één of het ander.

Als in een functie/methode een if-statement voorkomt, moet dat if-statement het eerste statement van de functie zijn

De verschillende vertakkingen binnen een if-statement moet je zo veel mogelijk door afzonderlijke functie/methoden laten afhandelen. In de volgende functie zie je drie if-statements staan.

import math as m

def report_primes(n):
    for i in range(2, n):
        if is_prime(i):
            print(i)

def is_prime(i):
    s = m.floor(m.sqrt(i)) + 1
    has_factor = False
    for f in range(2, s):
        if i % f == 0:
            has_factor = True
    if has_factor ==  False:
        return True        

report_primes(100)

Als je het eerste if_statement vervangt door een aparte functie, krijg je

import math as m

def report_primes(n):
    for i in range(2, n):
        report_prime(i)

def report_prime(i):
        if is_prime(i):
            print(i)

def is_prime(i):
    s = m.floor(m.sqrt(i)) + 1
    has_no_factor = True
    for f in range(2, s):
        if i % f == 0:
            has_no_factor = False
    return has_no_factor

report_primes(100)

Als ik de andere if-statements in een aparte functie probeer onder te brengen, vind ik het programma er niet duidelijker op worden.

Vermijd elk else-statement

De regel luidt: Gebruik geen else in een if-statement, behalve wanneer we te maken hebben met een variabele waarvan we de inhoud niet kunnen bepalen (bijvoorbeeld het indrukken van toets). De reden om else zo min mogelijk te gebruiken is omdat het verwarrend kan zijn onder welke omstandigheden tot de else-vertakking wordt overgegaan. In onderstaand programma

def gemiddelde(getallen_reeks):
    if len(getallen_reeks) == 0:
        print('een lege getallenreeks is niet toegestaan')
    else:
        return sum(getallen_reeks) / len(getallen_reeks)

def main():
    print(gemiddelde([1, 2, 3]))
    print(gemiddelde([100, 200, 300]))
    print(gemiddelde([]))

if __name__ == '__main__':
    main()

vervangen we de vertakkingen van de if- en else-clausules door opeenvolgende functie-aanroepen. Elke van die functies begint met een if-statement zonder else-clausule.

def gemiddelde(getallen_reeks):
    check_getallen_reeks_leeg(getallen_reeks)
    return bereken_gemiddelde(getallen_reeks)

def check_getallen_reeks_leeg(getallen_reeks):
    if len(getallen_reeks) == 0:
        print('een lege getallenreeks is niet toegestaan')
        return None

def bereken_gemiddelde(getallen_reeks):
    if len(getallen_reeks) != 0:
        return sum(getallen_reeks) / len(getallen_reeks)

def main():
    print(gemiddelde([1, 2, 3]))
    print(gemiddelde([100, 200, 300]))
    print(gemiddelde([]))

if __name__ == '__main__':
    main()

De output van beide programma's is hetzelfde:

2.0
200.0
een lege getallenreeks is niet toegestaan
None

Als je deze werkwijze hanteert, wordt de programmatuur er dan duidelijker op? Als je niet in de gaten hebt, dat de functies check_getallen_reeks_leeg() en bereken_gemiddelde() beide met een if-statement beginnen, en de voorwaarden die bij die if-statements horen samen alle mogelijkheden vertegenwoordigen ( x == 0 en x != 0 vertegenwoordigen samen alle mogelijke waarden die x kan aannemen ), dan lijkt de inhoud van de functie gemiddelde() op een sequentie van een aantal acties.

Gebruik geen elif

Het boek 'Five lines of code' nodigt uit om onderscheid te maken tussen checks en decisions. Een if_statement is een check als er enkel gekeken wordt of er een bepaald iets aan de hand is. Een if-elif-statement kun soms meer opvatten als decision, als een beslissing die genomen moet worden. De filosofie is dat je beslissingen zo lang mogelijk moet uitstellen, dus zo laat mogelijk moet nemen. Om elif-clausules te vermijden propageert het boek 'Five lines of code' het refactoring-pattern 'Replace code with classes'.

Breng verschillende typen gegevens onder in classes

Als je in een if-elif-constructie te maken hebt met verschillende categorieën, bijvoorbeeld met rood, oranje en groen (bij stoplichten) of small, medium en large (bij kledingmaten), breng dan elk van die categorieën onder in een eigen class. Als voorbeeld nemen we het volgende programma:

def comment_bank(bank):
    if bank.upper() == 'ASN':
        print('ASN')
        print('duurzaam en geen wapenindustrie')
    elif bank.upper() == 'TRIODOS':
        print('TRIODOS')
        print('duurzaam')
    elif bank.upper() == 'ING':
        print('ING')
        print('voert oranje leeuw als mascotte')
    elif bank.upper() == 'RABO':
        print('RABO')
        print('was ooit coöperatief')
    elif bank.upper() == 'ABN':
        print('ABN')
        print('werd ooit opgekocht door de overheid')

def main():
    bank = input('Bank ')
    comment_bank(bank)

if __name__ == '__main__':
    main()

De gebruiker wordt gevraagd de naam van een bank in te voeren. Vervolgens wordt wat informatie over de bank getoond. De functie comment_bank() bevat een uitgebreid if-elif-else-statement. Dit programma gaan we in een aantal stappen herschrijven. Als een gebruiker ASN, Asn, asn of iets dergelijks intikt, verschijnt de tekst

ASN
duurzaam en geen wapenindustrie
Voor deze bank maken we een class, waarin wat de user specificeert (de naam van de bank) voorkomt als attribuut, en het resultaat (het tonen van informatie) voorkomt als methode. Je krijgt:
class Asn_bank():
    name = 'ASN' 
   
    def process_bank(self):
        print('ASN')
        print('duurzaam en geen wapenindustrie')

Iets soortgelijks doen we voor de andere banken die een gebruiker kan kiezen. We zorgen ervoor dat in de nieuwe classes het attribuut en de methode dezelfde naam hebben. We krijgen dan:

class Asn_bank():
    name = 'ASN'
    
    def process_bank(self):
        print('ASN')
        print('duurzaam en geen wapenindustrie')

class Triodos_bank():
    name = 'TRIODOS'

    def process_bank(self):
        print('TRIODOS')
        print('duurzaam')

class Ing_bank():
    name = 'ING'

    def process_bank(self):
        print('ING')
        print('voert oranje leeuw als mascotte')

class Rabo_bank():
    name = 'RABO'

    def process_bank(self):
        print('RABO')
        print('was ooit coöperatief')

class Abn_bank():
    name = 'ABN'

    def process_bank(self):
        print('ABN')
        print('werd ooit opgekocht door de overheid')

Elke class heeft een attribuut name en een methode process_bank(). Het programma begint met het maken van een object voor elke class.

    asn = Asn_bank()
    triodos = Triodos_bank()
    ing = Ing_bank()
    rabo = Rabo_bank()
    abn = Abn_bank()

We nemen deze objecten op in een list.

    bank_list = [asn, triodos, ing, rabo, abn]

Dit doen we, omdat we daarmee de verschillende classes één voor één kunnen benaderen. We vragen aan een gebruiker een banknaam in te tikken.

bank_input = input('Bank ')

Daarna doorlopen we de lijst bank-objecten. Als we bij de bank zijn aangekomen, die de gebruiker heeft gespecificeerd, voeren we de methode process_bank() uit.

Het programma ziet er in zijn geheel dan als volgt uit:

class Asn_bank():
    name = 'ASN'
    
    def process_bank(self):
        print('ASN')
        print('duurzaam en geen wapenindustrie')

class Triodos_bank():
    name = 'TRIODOS'

    def process_bank(self):
        print('TRIODOS')
        print('duurzaam')

class Ing_bank():
    name = 'ING'

    def process_bank(self):
        print('ING')
        print('voert oranje leeuw als mascotte')

class Rabo_bank():
    name = 'RABO'

    def process_bank(self):
        print('RABO')
        print('was ooit coöperatief')

class Abn_bank():
    name = 'ABN'

    def process_bank(self):
        print('ABN')
        print('werd ooit opgekocht door de overheid')

def main():

    # Maak bank-instellingen
    asn = Asn_bank()
    triodos = Triodos_bank()
    ing = Ing_bank()
    rabo = Rabo_bank()
    abn = Abn_bank()
    
    # Maak een lijst met banken
    bank_list = [asn, triodos, ing, rabo, abn]
       
    # Tik een bank in
    bank_input = input('Bank ')
    for bank in bank_list:
        if bank_input.strip().upper() == bank.name.upper():
            bank.process_bank()

if __name__ == '__main__':
    main()

We hebben nu het programma zodanig herschreven dat het if-elif-statement vervangen is door een if-statement zonder elif-clausules.
Als we gebruik maken van een dictionary in plaats van een list, kan het programma zonder if-statement worden geschreven.

class Asn_bank():
    
    def process_bank(self):
        print('ASN')
        print('duurzaam en geen wapenindustrie')

class Triodos_bank():
    
    def process_bank(self):
        print('TRIODOS')
        print('duurzaam')

class Ing_bank():

    def process_bank(self):
        print('ING')
        print('voert oranje leeuw als mascotte')

class Rabo_bank():

    def process_bank(self):
        print('RABO')
        print('was ooit coöperatief')

class Abn_bank():

    def process_bank(self):
        print('ABN')
        print('werd ooit opgekocht door de overheid')

def main():

    # Maak bank-instellingen
    asn = Asn_bank()
    triodos = Triodos_bank()
    ing = Ing_bank()
    rabo = Rabo_bank()
    abn = Abn_bank()
    
    # Maak dictionary met bank-instellingen

    bank_dict = {'ASN': asn,
                 'TRIODOS': triodos,
                 'ING': ing,
                 'RABO': rabo,
                 'ABN': abn
                }

    bank_input = input('bank ')
    try:
        bank_dict[bank_input.upper()].process_bank()
    except:
        pass

if __name__ == '__main__':
    main()

Breng code onder bij classes

Als voorbeeld nemen we het volgende programma:

def handle_event(invoer):
    print('handle_input(' + invoer + ')')
    if invoer == 'a':             
        print('naar links')
        move_horizontal(-1)
    elif invoer == 'd':
        print('naar rechts')
        move_horizontal(1)
    elif invoer == 'w':
        print('omhoog')
        move_vertical(-1)
    elif invoer == 's':
        print('omlaag')
        move_vertical(1)
    
def move_horizontal(x):
    print('move horizontal ' + str(x))
    
def move_vertical(y):
    print('move vertical ' + str(y))
    
def main():
    invoer = input('a=links, d=rechts, w=omhoog, s=omlaag ')
    handle_event(invoer)

if __name__ == '__main__':
    main()

Als een gebruiker een van de toetsen a, w, d of s indrukt, gaat er iets op het scherm naar links, naar boven , naar rechts of naar beneden. Het op en neer of omhoog en omlaag gaan wordt verzorgd door de functies move_vertical() en move_horizontal(). We willen in dit programma de functies move_horizontal() en move_vertical() dichterbij elkaar brengen. Daartoe creëren we voor elk van de vier mogelijkheden 'naar links', 'naar rechts, 'naar boven' en 'naar beneden' een class.

class Left(Invoer):

    def handle():
        move_horizontal(-1) 

class Right(Invoer):

    def handle():
        move_horizontal(1)

class Up(Invoer):

    def handle():
        move_vertical(-1)

class Down(Invoer):

    def handle():
        move_vertical(1)

Deze vier classes hebben dezelfde structuur. Merk op dat we bij bij de method handle() geen parameter self meegeven. Dat betekent dat het een class-method is. We hoeven geen gebruik te maken van objecten die zijn gebaseerd op Left, Right, Up en Down. De oorspronkelijke code herschrijven we nu.

class Left():

    def handle():
        move_horizontal(-1)
        
class Right():

    def handle():
        move_horizontal(1)
            
class Up():

    def handle():
        move_vertical(-1)
            
class Down():

    def handle():
        move_vertical(1)
            
def move_horizontal(x):
    print('move_horizontal(' + str(x) + ')')
    
def move_vertical(y):
    print('move_vertical(' + str(y) + ')')
    
def main():
    prompt = 'a=links, d=rechts, w=omhoog, s=omlaag '
    invoer = input(prompt)
    if invoer == 'a':
        invoer_class = Left
    elif invoer == 'd':
        invoer_class = Right
    elif invoer == 'w':
        invoer_class = Up
    elif invoer == 's':
        invoer_class = Down
    invoer_class.handle()

if __name__ == '__main__':
    main()

Als een gebruiker een keuze heeft gemaakt, wordt afhankelijk wat hij heeft ingetikt, in de variabele invoer_class een verwijzing gemaakt naar één van de classes Left, Right, Up or Down. Omdat in elk van die classes de method handle() een andere inhoud heeft, wordt steeds de bijbehorende move-actie uitgevoerd.

De code van een functie in een andere functie opnemen

In het volgende programma lijkt de functie rekening_bijwerken() overbodig.

class database:
    def update_rekening(rekening, bedrag):
        print('update_rekening :', bedrag, '->', rekening)
        
def rekening_bijwerken(rekening, bedrag):
    database.update_rekening(rekening, bedrag)

def overboeken(vanaf_rekening, naar_rekening, bedrag):
    rekening_bijwerken(vanaf_rekening, -bedrag)
    rekening_bijwerken(naar_rekening, bedrag)

def main():
    overboeken('ABN123', 'ING456', 543.21)
    
if __name__ == '__main__':
    main()

Je kunt de code van de functie rekening_bijwerken() direct opnemen in de functie overboeken().

class database:
    def update_rekening(rekening, bedrag):
        print('update_rekening :', bedrag, '->', rekening)
        
def overboeken(vanaf_rekening, naar_rekening, bedrag):
    database.update_rekening(vanaf_rekening, -bedrag)
    database.update_rekening(naar_rekening, bedrag)

def main():
    overboeken('ABN123', 'ING456', 543.21)
    
if __name__ == '__main__':
    main()

Splits een functie of method op in meerdere functies of methoden

Soms wordt de programmatuur begrijpelijker als een ingewikkelde functie wordt opgesplitst in functies die elk wat eenvoudiger te begrijpen zijn. In een spel kan een functie die antwoord geeft op de vraag 'welke vervolgacties zijn mogelijk?' soms beter gesplitst worden in bijvoorbeeld 'welke vervolgacties zijn mogelijk op de linkerflank?' en 'welke vervolgacties zijn mogelijk op de rechterflank?'.

Gebruik overerving alleen bij interfaces

Het woordenboek geeft als vertaling voor interface de woorden grensvlak, raakvlak en koppeling. In deze paragraaf gaat het over interfaces zoals die voorkomen in de programmeertaal Java. Interfaces in Java komen overeen met classes in Python, die als blauwdruk fungeren voor andere classes. Ze geven aan welke methods een class moet hebben, maar niet wat die methods moeten doen. Vertaald naar Python bevatten de methods van een interface enkel het statement pass. Volgens de regel 'Gebruik overerving alleen bij interfaces' moet je alleen overerving gebruiken als de superclass enkel de namen van de te definiëren methoden doorgeeft. Wat die methoden doen, moet je in de class zelf opgeven.
Overerving wordt vaak gebruikt om een default implementatie van een method te krijgen. De nadelen daarvan zijn vaak veel ingrijpender dan de voordelen. Code die door verschillende subklassen wordt gebruikt, veroorzaakt koppelingen.
Vrij vertaald betekent dit: Gebruik overerving zo min mogelijk; geef de vookeur aan composition.

Verwijder niet gebruikte functies en methods

Er zijn een aantal pakketten waarmee je functies en methodes die nergens worden aangeroepen in een Python-programma, kunt opsporen. Op internet vond ik (op 11-11-2024):

Voeg soortgelijke classes samen

Ik kan mij goed voorstellen dat er situaties zijn, dat een programma begrijpelijker en beter onderhoudbaar is, wanneer wordt onderkend dat een aantal classes beter samengevoegd kunnen worden tot één class. Maar het is mij niet gelukt om in Python van een werkend programma te verzinnen, dat eenvoudig en kort genoeg is, en niet gekunsteld, om als voorbeeld te dienen.

Vermijd identieke vertakkingen van if-statements

Het volgende code-fragment bevat twee vertakkingen waarop de code 'x = 0'volgt.

if a > 0:
    x = 0
if a == 0:
    x = 1
if a < 0:
    x = 0

Deze code kun je herschrijven tot

if a == 0:
    x = 1
else
    x = 0

of

x = 0
if a == 0:
    x = 1

Welke codering je voorkeur heeft is een beetje een kwestie van smaak. De volgende oplossing die 'or' gebruikt wordt aanbevolen door de schrijver van het boek 'Five lines of code' omdat in alle vertakkingen van het if-statement een andere code staat en uit de code direct is af te lezen in welke gevallen welke vertakkingen worden aangeroepen..

if a < 0 or a > 0:
    x = 0
if a == 0:
    x = 1

Het is van belang de if-statements zo begrijpelijk mogelijk te houden. Soms is het prettig een voorwaarde in een aparte functie te evalueren.

def check_perioden_overlappen_elkaar(
        periode_1_vanaf, periode_1_tm, periode_2_vanaf, periode_2_tm):
    if periode_2_tm < periode_1_vanaf or periode_1_tm < periode_2_vanaf:
        return False
    else:
        return True
    
def main():
    print('Overlappen twee perioden elkaar?')
    periode_1_vanaf = input('Eerste periode, vanaf-datum (JJJJMMDD) ')
    periode_1_tm = input('Eerste periode, t/m-datum   (JJJJMMDD) ' )
    periode_2_vanaf = input('Tweede periode, vanaf-datum (JJJJMMDD) ')
    periode_2_tm = input('Tweede periode, t/m-datum   (JJJJMMDD) ')
    print(' ')
    print('Eerste periode: ', periode_1_vanaf, 't/m', periode_1_tm)
    print('Tweede periode: ', periode_2_vanaf, 't/m', periode_2_tm)
    if check_perioden_overlappen_elkaar(
            periode_1_vanaf, periode_1_tm, periode_2_vanaf, periode_2_tm):
        print('De perioden overlappen elkaar')
    else:
        print('De perioden overlappen elkaar niet.')

if __name__ == '__main__':
    main()

Vermijd situaties met zij-effecten

Met 'situaties met zij-effecten' bedoelen we condities die waarden toekennen aan variabelen, foutsituaties genereren, iets afdrukken, iets naar een file wegschrijven, e.d.
Als ik een bestand ga inlezen op de volgende manier

def lees_bestand():
    file_name = 'een_csv_bestand.csv'
    file_object = open(file_name)
    line = file_object.readline()
    print(line)
    while line != '':
        line = file_object.readline()
        print(line)
    file_object.close()
    
lees_bestand()

dan lees ik regel voor regel in, en gebeuren er iedere keer als ik een volgende regel inlees twee dingen:

  1. de gegevens van de nieuwe regel worden opgeslagen in variabele line en
  2. de gegevens van de vorige regel verdwijnen uit line, en zijn daarmee in de rest van het programma niet meer bekend.
Het tweede effect is een zij-effect. Vaak moet je extra maatregelen nemen om een zij-effecten te beteugelen. Laten we voor bovenstaand programma het volgende bestand als input nemen.

David;20240112;35.00
Jim;20240123;44.00
Ken;20240125;32.81
John;20240131;4.33
David;20240204;35.00
John;20240205;21.61
Ken;20240221;12.32
David;20240228;16.37

De output wordt dan

DDavid;20240112;35.00

Jim;20240123;44.00

Ken;20240125;32.81

John;20240131;4.33

David;20240204;35.00

John;20240205;21.61

Ken;20240221;12.32

David;20240228;16.37

Veronderstel nu dat er een verzoek ligt om de lege regels uit het overzicht te halen. Voor dat verzoek heb je geen last van het zij-effect. Je kunt de onzichtbare tekens \r en \n (CR en LF) uit de print-statements halen.

def lees_bestand():
    file_name = 'een_csv_bestand.csv'
    file_object = open(file_name)
    line = file_object.readline()
    print(line, end='')
    while line != '':
        line = file_object.readline()
        print(line, end='')
    file_object.close()
    
lees_bestand()

De output wordt dan:

David;20240112;35.00
Jim;20240123;44.00
Ken;20240125;32.81
John;20240131;4.33
David;20240204;35.00
John;20240205;21.61
Ken;20240221;12.32
David;20240228;16.37

Je kunt ook de tekens \r en \n (CR en LF) uit de variabele line zelf halen.

def lees_bestand():
    file_name = 'een_csv_bestand.csv'
    file_object = open(file_name)
    line = file_object.readline().rstrip()
    print(line)
    while line != '':
        line = file_object.readline().rstrip()
        print(line)
    file_object.close()
    
lees_bestand()

Laten we nu aannemen dat het input-bestand bedragen voorstelt die David, Jim, Ken en John hebben betaald op verschillende dagen. We willen nu de totaalbedragen die David, Jim, Ken en John hebben betaald berekenen. Nu krijgen we wel 'last' van het zij-effect dat gegevens verdwijnen bij elke inlees-actie. We moeten nu een stuk geheugen reserveren om de gegevens te bewaren, die anders zouden verdwijnen. Dit chache-geheugen kan allerlei vormen krijgen. In het volgende voorbeeld wordt een dictionary als cache-geheugen gebruikt, waarin de tussentotalen per persoon worden bijgehouden.

import decimal

def lees_bestand():
    cache = {}                                  # 
    file_name = 'een_csv_bestand.csv'
    file_object = open(file_name)
    line = file_object.readline().rstrip()
    while line != '':
        fields = line.split(';')                   
        name = fields[0]                        # 
        amount = decimal.Decimal(fields[2])     # 
        if name in cache:                       # 
            cache[name] += amount               # 
        else:                                   # 
            cache[name] = amount                # 
        line = file_object.readline().rstrip()    
    file_object.close()
    print( cache.items() )                      # 
    
lees_bestand()

Bij definieer je cache-geheugen als dicionary. Bij geef je het eerste item in een regel de naam name. Bij zet je het derde item in een regel om naar een decimaal getal dat je amount noemt. Als het eerste item van de regel al in cache voorkomt dan tel je amount op bij het bedrag dat daar al staat, anders maak je een nieuw item aan in cache. Bij laat je de inhoud van de dictionary zien. De output wordt:

dict_items([('David', Decimal('86.37')), ('Jim', Decimal('44.00')), ('Ken', Decimal('45.13')), ('John', Decimal('25.94'))])

De dictionary kun je iets netter afdrukken:

import decimal
from pprint import pprint

def lees_bestand():
    cache = {} 
    file_name = 'een_csv_bestand.csv'
    file_object = open(file_name)
    line = file_object.readline().rstrip()
    while line != '':
        fields = line.split(';')
        name = fields[0]
        amount = decimal.Decimal(fields[2])
        if name in cache:
            cache[name] += amount
        else:
            cache[name] = amount
        line = file_object.readline().rstrip()
    file_object.close()
    pprint(cache)

lees_bestand()

geeft output

{'David': Decimal('86.37'),
 'Jim': Decimal('44.00'),
 'John': Decimal('25.94'),
 'Ken': Decimal('45.13')}

Strategy pattern

Als verschillende functies heel erg op elkaar lijken, kun je ze vaak samenvoegen met het strategy pattern. Vergelijk in het volgende programma de methoden kleinste() en som().

def kleinste(lijst):
    kleinste_tot_nu_toe = lijst[0]
    i = 0    
    while i < len(lijst):
        if kleinste_tot_nu_toe > lijst[i]:
             kleinste_tot_nu_toe = lijst[i]
        i += 1
    return kleinste_tot_nu_toe

def som(lijst):
    som_tot_nu_toe = 0
    i = 0    
    while i < len(lijst):
        som_tot_nu_toe += lijst[i]
        i += 1
    return som_tot_nu_toe

if __name__ == '__main__':
    lijst = [5, 7, 9, 3, 11, 13, 15]
    print( kleinste(lijst) )   # 3
    print( som(lijst) )        # 63

Als je het while-statement vervangt door een for-statement, hoef je de hulp-variabele i niet meer te gebruiken. Voorts veranderen we de variabelen kleinste_tot_nu_toe en som_tot_nu_toe. We noemen ze beide tussenstand.

def kleinste(lijst):
    tussenstand = lijst[0]
    for item in lijst:
        if tussenstand > item:
             tussenstand = item
    return tussenstand

def som(lijst):
    tussenstand = 0
    for item in lijst:
        tussenstand += item
    return tussenstand

if __name__ == '__main__':
    lijst = [5, 7, 9, 3, 11, 13, 15]
    print( kleinste(lijst) )   # 3
    print( som(lijst) )        # 63

In bovenstaande code zijn twee gedeeltes vetgedrukt. Deze delen gaan we opnemen in een callable class.

class ProcessKleinste:
    def __init__(self, item):
        self.item = item
        self.tussenstand = lijst[0]

    def __call__(self):
        if self.tussenstand > self.item:
             self.tussenstand = self.item

Voorafgaand aan het for-statement maken we een object van class ProcessKleinste. Dit object noemen we process_kleinste. Binnen de for-loop roepen we process_item aan, waardoor we de methode __call__() uitvoeren.

def kleinste(lijst):
    process_kleinste = ProcessKleinste()
    for item in lijst:
        eindstand = process_kleinste(item)
    return eindstand

class ProcessKleinste:
    def __init__(self):
        self.tussenstand = lijst[0]

    def __call__(self, item):
        if self.tussenstand > item:
             self.tussenstand = item
        return self.tussenstand

def som(lijst):
    tussenstand = 0
    for item in lijst:
        tussenstand += item
    return tussenstand

if __name__ == '__main__':
    lijst = [5, 7, 9, 3, 11, 13, 15]
    print( kleinste(lijst) )
    print( som(lijst) )

Iets soortgelijks doen we bij de functie som().

def kleinste(lijst):
    process_kleinste = ProcessKleinste()
    for item in lijst:
        eindstand = process_kleinste(item)
    return eindstand

class ProcessKleinste:
    def __init__(self):
        self.tussenstand = lijst[0]

    def __call__(self, item):
        if self.tussenstand > item:
             self.tussenstand = item
        return self.tussenstand

def som(lijst):
    process_som = ProcessSom()
    for item in lijst:
        eindstand = process_som(item)
    return eindstand

class ProcessSom:
    def __init__(self):
        self.tussenstand = 0

    def __call__(self, item):
        self.tussenstand += item
        return self.tussenstand

if __name__ == '__main__':
    lijst = [5, 7, 9, 3, 11, 13, 15]
    print( kleinste(lijst) )  # 3
    print( som(lijst) )       # 63

Je zou de lijst ook éénmalig kunnen doorlopen:

class ProcessKleinste:
    def __init__(self):
        self.tussenstand = lijst[0]

    def __call__(self, item):
        if self.tussenstand > item:
             self.tussenstand = item
        return self.tussenstand

class ProcessSom:
    def __init__(self):
        self.tussenstand = 0

    def __call__(self, item):
        self.tussenstand += item
        return self.tussenstand

def process_lijst(lijst):
    process_kleinste = ProcessKleinste()
    process_som = ProcessSom()
    for item in lijst:
        eindstand_kleinste = process_kleinste(item)
        eindstand_som = process_som(item)
    return eindstand_kleinste, eindstand_som

if __name__ == '__main__':
    lijst = [5, 7, 9, 3, 11, 13, 15]
    print( process_lijst(lijst) )   # (3, 63)

Je kunt de verschillende code-fragmenten ook als volgt combineren:

class ProcessKleinste:
    def __init__(self):
        self.tussenstand = lijst[0]

    def __call__(self, item):
        if self.tussenstand > item:
             self.tussenstand = item
        return self.tussenstand

class ProcessSom:
    def __init__(self):
        self.tussenstand = 0

    def __call__(self, item):
        self.tussenstand += item
        return self.tussenstand

def process_batch(lijst, Processor):
    process = Processor()
    for item in lijst:
        eindstand = process(item)
    return eindstand

if __name__ == '__main__':
    lijst = [5, 7, 9, 3, 11, 13, 15]
    print( process_batch(lijst, ProcessKleinste) ) # 3
    print( process_batch(lijst, ProcessSom) )      # 63

Het kan nooit kwaad om na te denken over een betere naamgeving. Het is usance om vooral engelse benamingen te gebruiken. Dan krijg je bijvoorbeeld:

class MinimumProcessor:
    def __init__(self):
        self.intermediate_value = batch[0]

    def __call__(self, item):
        if self.intermediate_value > item:
             self.intermediate_value = item
        return self.intermediate_value

class SumProcessor:
    def __init__(self):
        self.intermediate_value = 0

    def __call__(self, item):
        self.intermediate_value += item
        return self.intermediate_value

def process_batch(batch, Processor):
    process = Processor()
    for item in batch:
        final_value = process(item)
    return final_value

if __name__ == '__main__':
    batch = [5, 7, 9, 3, 11, 13, 15]
    print( process_batch(batch, MinimumProcessor) )
    print( process_batch(batch, SumProcessor) )

Law of Demeter

De 'law of Demeter' zoals die op internet wordt uitgelegd is niet altijd toepasbaar als je gebruik maakt van de computertaal Python. Je vindt op internet beschrijvingen als: De 'Law of Demeter' vereist dat een methode meth van object objct alleen methoden mag aanroepen van de volgende soorten objecten:

Als je verder geen gebruik maakt van globale varabelen, dwingt Python je voor zover ik weet in bijna alle gevallen om aan deze voorwaarden te voldoen. Het basis-idee achter de 'law of Demeter' is dat een gegeven object zo min mogelijk moet weten over de structuur of eigenschappen van iets anders. Dat komt overeen met het principe van 'information hiding', wat dicteert dat een module alleen toegang heeft tot de informatie die het nodig heeft voor zijn werkelijke doel.
Een object a kan een methode van een object b aanroepen, maar object a zou daardoor niet (onopgemerkt) toegang moeten krijgen tot informatie over over een object c, en daar dan weer een methode van aanroepen. Dat zou betekenen dat object a meer zou moeten weten over hoe object b intern gestructureerd is.
Een consequentie is: Een object moet geen methoden van een object aanroepen die de return-waarde is van een andere methode. In object-georienteerde talen die een punt als verbindings-identifier gebruiken, zegt de 'law of Demeter' 'gebruik slechts één punt'. Dat wil zeggen, dat de code a.m().n() de 'law of Demeter'schendt, terwijl a.m() dat niet doet. Populair gezegd: Als je een hond commandeert te gaan lopen, commandeer je niet de poten van de hond om te gaan lopen, in plaats daarvan commandeer je de hond te gaan lopen, en de hond stuurt dan weer zelf zijn poten aan.
Het volgende voorbeeld laat zien hoe deze populaire zegswijze in python uitpakt:

class Baas():
    def __init__(self, naam):
        self.naam = naam

    def commandeer_hond_rechter_voorpoot_te_bewegen(self, hond):
        hond._beweeg_rechter_voorpoot()

class Hond():
    def __init__(self, naam):
        self.naam = naam

    def _beweeg_rechter_voorpoot(self):
        print('de hond beweegt rechter voorpoot')

    def _beweeg_linker_voorpoot(self):
        print('de hond beweegt linker voorpoot')

    def _beweeg_rechter_achterpoot(self):
        print('de hond beweegt rechter achterpoot')

    def _beweeg_linker_achterpoot(self):
        print('de hond beweegt rechter achterpoot')

    def lopen(self):
        self._beweeg_rechter_voorpoot()
        self._beweeg_linker_achterpoot()
        self._beweeg_linker_voorpoot()
        self._beweeg_rechter_achterpoot()
        print('de hond loopt')

def main():
    jan = Baas('Jan')
    fik = Hond('Fik')
    jan.commandeer_hond_rechter_voorpoot_te_bewegen(fik)

if __name__ == '__main__':
    main()

De output is:

de hond beweegt rechter voorpoot

Het programma lijkt goed te werken. Maar in de regel 'hond._beweeg_rechter_voorpoot()' wordt een methode aangeroepen, die met een underscore begint. Met zo'n underscore aan het begin van een methode geeft een programmeur aan dat het niet de bedoeling is, dat de methode niet door een object buiten buiten het Hond-object wordt aangeroepen. Om dat het object jan is geen Hond-object is, schendt het programma de 'law of Demeter'. jan mag geen methode aanroepen die bedoeld is voor intern gebruik binnen de class Hond.

In programma's waarin hiërarchieën van objecten worden gedefinieerd, of anderszins afstanden tussen objecten bestaan, dan is het verstandig de 'law of Demeter' in acht te nemen. De 'law of Demeter' wordt ook wel geformuleerd als 'spreek niet met vreemden' of 'spreek alleen met je buren'.
Je moet de 'law of Demeter' in gedachten houden, maar niet beschouwen als een dogma.

Getters en setters gebruik je niet in Python

In de programmeertaal Java is het gebruik van getters en setter standaard ingebouwd in de taal. In Python hebben getters en setters eigenlijk geen zin. Een variabele die gedefinieerd wordt binnen een class, kan voor zover de regels van de namespaces het toelaten, op de gewone manier gewijzigd worden.

class GetSetX:
    def __init__(self, x):
        self.x = x

def main():
    obj = GetSetX('obj')
    print( obj.x )         # obj
    obj.x = 'obj_new'
    print( obj.x )         # obj_new

if __name__ == '__main__':
    main()

In Java worden speciale methodes toegevoegd aan de classes, waarmee variabelen in objecten kunnen worden opgevraagd en gewijzigd.

class GetSetX:
    def __init__(self, x):
        self.x = x

    def get_x(self):
        return self.x

    def set_x(self, y):
        self.x = y
        return self.x

def main():
    obj = GetSetX('obj')
    print( obj.get_x() )    # obj
    obj.set_x('obj_new')
    print( obj.get_x() )    # obj_new

if __name__ == '__main__':
    main()

Als je de laatste drie regels van main() verplaatst naar een method van een nieuwe class U, moet je het object obj global maken, anders kan obj niet benaderd worden vanuit class U.

class GetSetX:
    def __init__(self, x):
        self.x = x

    def get_x(self):
        return self.x

    def set_x(self, y):
        self.x = y
        return self.x

class U:
    def __init__(self, u_name):
        self.u_name = u_name
        
    def change_x(self):
        print( obj.get_x() )    # obj
        obj.set_x('obj_new')
        print( obj.get_x() )    # obj_new
    
def main():
    global obj
    obj = GetSetX('obj')
    u = U('u_name')
    u.change_x()

if __name__ == '__main__':
    main()

Een globale variabele kun je overal vandaan wijzigen. Als je dat op meerdere plaatsen in je programma doet, is de kans groot dat je op een gegeven moment geen idee meer hebt door welk commando de variabele werd gewijzigd. Het wijzigen van globale variabelen is vragen om moeilijkheden. De regel 'Do not use getters or setters' zou je in Python moeten vervangen door 'Gebruik geen variabelen of methoden die enkel voor intern gebruik binnen de objecten zelf bedoeld zijn'.